From ed95ae6398df32859c1eb7896f6db0ddfa867335 Mon Sep 17 00:00:00 2001 From: Ahmed Sayed Date: Wed, 17 Apr 2019 12:27:23 +0200 Subject: [PATCH] Feature download only (#810) * Added initial version of DOWNLOAD_ONLY * Added DOWNLOAD_ONLY option to ActionTypeOptionGroupLayout * Removed DOWNLOAD_ONLY checkbox, added Download Only UI option * Mark actions that finished with DOWNLOADED as finished * initial changes to realize downoadOnly in UI * Changed method of disabling maintenanceWindow into smarter solution * Added new icon for download only option * Set DistributionSet as unassigned when DOWNLOAD_ONLY * Enabled update action status for DOWNLOAD_ONLY after download * Current state of abstraction task * Assign DistributionSet to target if target installs it after downloading * Abstracted class redundant methods * Added tests * Fixed Rollout finish status for DWONLOAD_ONLY Rollouts * Added Rollout type json property in test documentation * Added DOWNLOAD_ONLY test for target assignment * Added event listener also to DistributionTable * Fixed event listener problem * Change column name to "Type" and added also DownloadOnly icon to that column. * Cleanup * Center aligned the icons in type column * Fixed DistributionSet already assigned but not installed * Rename download_only to downloadonly * Further changes regarding center aligned the icons * Fixed target assign status in Rollout view when download_only * Fixed SonarQube issues * Fixed SonarQube issues + code formatting * Fixed Tests * Marked squid:S128 as suppressed - irrelevant * Adapting rollouts view by additional column (not finished by now) * Putted type column on proper position * Trying to display icons in new type column in rollouts view * Added icon also for soft, icon might change -> just change * createOptionGroup method in ActionTypeOptionGroupLayout class * added first draft of type column in rollouts view * increase visibility of sendUpdateMessageToTarget method * Ground functionality of new type column in deployment view is now implemented * Type column implementation in rollouts view is finished for now * Rebased on master * Fixed DurationControl change on ScheduleControl change. * (Re)Added Soft deployment Icon * Fixed SonarQube issues * Fixed SonarQube issues * Fixed failing test * Fixes + added missing header * Added message to the fail() instruction * Fixed copyright header * Apply suggestions from code review * Fixed TotalTargetCountStatus.java * Removed unused method from TotalTargetCountStatus.java * add id to rollout create and update UI popup * Added download_only tests for MgmtTargetResourceTest.java * added missing header in TotalTargetCountStatusTest.java * Rename because of newest changes * added Download_Only dmf integration tests * Renamed MgmtAction.forcedType to actionType * renamed actionType to forceType for Mgmt API * added missing javadocs for public methods * Added Download Only support for AutoAssignment Signed-off-by: Ahmed Sayed Signed-off-by: Ammar Bikic --- docs/content/ui.md | 2 +- .../ui/target_filter_auto_assignment.png | Bin 112166 -> 96285 bytes .../hawkbit/exception/SpServerError.java | 2 +- .../AmqpAuthenticationMessageHandler.java | 1 - .../hawkbit/amqp/AmqpConfiguration.java | 6 + .../amqp/AmqpMessageDispatcherService.java | 37 +- .../amqp/AmqpMessageHandlerService.java | 5 +- .../amqp/AmqpMessageHandlerServiceTest.java | 24 +- .../AbstractAmqpServiceIntegrationTest.java | 15 +- ...ssageDispatcherServiceIntegrationTest.java | 40 ++ ...pMessageHandlerServiceIntegrationTest.java | 108 +++++ .../dmf/json/model/DmfActionUpdateStatus.java | 4 +- .../repository/RepositoryProperties.java | 9 +- .../TargetAssignDistributionSetEvent.java | 19 +- .../hawkbit/repository/model/Action.java | 7 +- .../repository/model/ActionProperties.java | 71 +++ .../repository/model/TargetFilterQuery.java | 2 +- .../model/TotalTargetCountStatus.java | 51 ++- .../model/TotalTargetCountStatusTest.java | 81 ++++ .../repository/jpa/ActionRepository.java | 12 +- .../jpa/JpaControllerManagement.java | 77 +++- .../jpa/JpaRolloutGroupManagement.java | 5 +- .../repository/jpa/JpaRolloutManagement.java | 47 +- .../jpa/builder/JpaRolloutGroupCreate.java | 70 ++- .../repository/jpa/model/JpaAction.java | 3 +- .../repository/jpa/model/JpaRollout.java | 5 +- .../repository/jpa/model/JpaRolloutGroup.java | 2 +- .../jpa/model/JpaTargetFilterQuery.java | 4 +- ...ThresholdRolloutGroupSuccessCondition.java | 4 +- .../remote/RemoteTenantAwareEventTest.java | 11 +- .../jpa/ControllerManagementTest.java | 405 ++++++++++++++++-- .../jpa/DeploymentManagementTest.java | 13 +- .../repository/jpa/RolloutManagementTest.java | 72 +++- .../jpa/TargetFilterQueryManagementTest.java | 10 + .../jpa/autoassign/AutoAssignCheckerTest.java | 46 +- .../test/util/AbstractIntegrationTest.java | 14 +- .../repository/test/util/TestdataFactory.java | 31 +- .../mgmt/json/model/action/MgmtAction.java | 12 +- .../action/MgmtActionRequestBodyPut.java | 12 +- .../model/distributionset/MgmtActionType.java | 7 +- .../rollout/MgmtRolloutResponseBody.java | 12 + ...gmtAssignedDistributionSetRequestBody.java | 3 +- .../rest/resource/MgmtRestModelMapper.java | 6 +- .../mgmt/rest/resource/MgmtRolloutMapper.java | 1 + .../mgmt/rest/resource/MgmtTargetMapper.java | 2 +- .../rest/resource/MgmtTargetResource.java | 2 +- .../MgmtDistributionSetResourceTest.java | 27 ++ .../resource/MgmtRolloutResourceTest.java | 30 +- .../MgmtTargetFilterQueryResourceTest.java | 43 +- .../rest/resource/MgmtTargetResourceTest.java | 20 + .../hawkbit/rest/util/JsonBuilder.java | 16 +- .../documentation/MgmtApiModelProperties.java | 1 + .../DistributionSetsDocumentationTest.java | 2 +- .../RolloutResourceDocumentationTest.java | 2 + ...ilterQueriesResourceDocumentationTest.java | 6 +- .../TargetResourceDocumentationTest.java | 2 +- .../hawkbit/ui/common/grid/AbstractGrid.java | 4 +- .../TargetAssignmentOperations.java | 286 +++++++++++++ .../actionhistory/ActionHistoryGrid.java | 68 ++- .../actionhistory/ActionStatusGrid.java | 2 +- .../actionhistory/ActionStatusMsgGrid.java | 2 +- .../management/dstable/DistributionTable.java | 174 +------- .../AbstractActionTypeOptionGroupLayout.java | 19 +- ...ActionTypeOptionGroupAssignmentLayout.java | 4 + ...onTypeOptionGroupAutoAssignmentLayout.java | 2 + .../management/targettable/TargetTable.java | 171 +------- .../targettable/TargetTableLayout.java | 8 +- .../rollout/AddUpdateRolloutWindowLayout.java | 1 + .../ui/rollout/rollout/ProxyRollout.java | 10 + .../ui/rollout/rollout/RolloutBeanQuery.java | 1 + .../ui/rollout/rollout/RolloutListGrid.java | 83 +++- .../rolloutgroup/RolloutGroupListGrid.java | 2 +- .../RolloutGroupTargetsListGrid.java | 14 +- .../ui/utils/UIComponentIdProvider.java | 11 +- .../hawkbit/ui/utils/UIMessageIdProvider.java | 4 + .../src/main/resources/messages.properties | 6 + 76 files changed, 1754 insertions(+), 639 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionProperties.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatusTest.java create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/TargetAssignmentOperations.java diff --git a/docs/content/ui.md b/docs/content/ui.md index 63fa86ab1..caf243a62 100644 --- a/docs/content/ui.md +++ b/docs/content/ui.md @@ -142,7 +142,7 @@ name==CCU* and updatestatus==pending ![Target Filter Management view](../images/ui/target_filter.png) ### Auto assignment -It is possible to assign some distribution set with different action types (_forced_ or _soft_) to all targets that belong to the corresponding custom target filter, including the ones, that are registered later on. +It is possible to assign some distribution set with different action types (_forced_, _soft_, or _download only_) to all targets that belong to the corresponding custom target filter, including the ones, that are registered later on. In order to activate the auto-assignment, one should first click on _Auto assignment_ cell in Custom Filters table, and then check the corresponding checkbox. After that, the action type and distribution set for auto-assignment should be selected and confirmed. diff --git a/docs/static/images/ui/target_filter_auto_assignment.png b/docs/static/images/ui/target_filter_auto_assignment.png index 2b195bd4c577055a085a35ef8c78a5bf2865d9f5..a6a5c0168f2ec2e75bb9c7373c66db212430b356 100644 GIT binary patch literal 96285 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�Nh6>xXni0 zq9vzIAG4lMqu$M+_^!_|H>ZwavS#caU zHIBaEQ@<}KYoTn0-IwP+1wqUZ_y;M5n?L4$1q=>HGeaPPym5Nj8&xI z!DCiL5vmnbdmu@Z-j{YpjDQ3P=wId6FW|eivO6yC>k&sG^E*_0 zisDz+AI~GKmp`4D7e83sZ|m7I^xVwb1kWUXL~4VPL^9f8<={EnlOYcJd+MklH523r zBI&^qCm>-@ZFI z==?z>A~NV@+4$SN_0s`LXYq$7&#l-vb6{d=L-VN!SR$~G5m2H% zPWjlNs3|V@lEomR@evm`j`2iVxVHmiw|xHEQPy+5RMH=g_k#@~1bF3m@5j z19WKLkhsZA%7j}Pp2B1-J{*4Yiu{aV!t1UltIs#9xFpCC;LDG-6=N%#A!aQl|m^VW{mu29A@) z$ex>|ho~zOHQdpeI6ytuIi=Y>@&}ye{UQ<@ee+ZK0yVKaNB9|ti-P~43ntzF&q6;@3k!_J^ zv`H;VztEiwmmhL&ENcrI=WjpaeON$!(X^L0e^z!cprZx9+7qjOzUE7z%3RU!H($mr zLqH4(%ba^~!3%w0`OsLU$RX~(X>Yi1vAZxSv16Aay}GzPv7Q#KW_V&5OX<7v^x52x z9l#-m@eeS3zYbUDB1MOV6Q+L~2H4^P8APqtde{UZQ>Y=*Vt;e8UeWRvccp&Z27G*&jN>bRX{nFOpm zl!)CwoCSC{TDGHe_}3I``xl?6N@kQaQJ+bFXdXTD`LY?>etqYv3B&=Y~3AEh-^{4&2$ygV4KNf>S-xE5q~43 z2BDCBt|LsN9U`tG2h27x6y*DotQ{MY7$8md7ND6s&Tk{)1^#R7Y7A zi4bL`4N|U5W$mhHUFJ&oohEyMS@4z7z7ITSe|GzMK4m=)*@HRMe4V!rE|bA6n7pCU z(188Gw3EH}Ud}*1=KG<&d#$KFel0#pfwCfQ=an!6G>|h2EH1*5=Hp<3s8LqRsu{^H z*kIb;TODgnms3avuj%=ZqMbpKZq>31BrV3lMtAb(ORZJgxm~r}V>wJ)Yeklvc!!}8n zGOGmBKIlq!x}FM^i7iET6xjXpE8c99cYfUFnqgn74x$W`N5+aH1?_-wkIqr zKV#7FJ1M&c0K?;iAAY1HvMVGU*14@sfj|RpUR~m@2={}-ToYZwMPGOsS$2PaLbc6h zd_8@L!SoS$+C46vS-UdlgQ#5@207zX$8x|0P~TL~Dczo{`F3@y&GhC?gCNRfPF);D zPZwW?KirXuc9^I5x%lgJF*|ybosHojXJo9sOM%GTk<)t~{TA!bFb>Y-r+7YhDEXn7fO{wdd>XbXf#Btqt1 zF_oY0CxfV1?g>(?$gOr#`i==~oiJi6GSSyfp{oAy zM{7oJDb@{G&)Xy1*DyLelt~#ze%u)MPUIORr$*ImNi9Y%4uk0Z&p>r&r%#HcS)`P_H>m&K2KTD)hjUzXMiw`FkV?I0VBR%FkyV$GAf=aN6cNLDoHSWNdip~SGz*)IT%E(3rNE4i?7&JV3Pm7;vyczcpH_v&rV-Kn&pUYcmn9nO)a5Wo#!PObVu^I4__Ez^-Uo;ggrR({$89Zr3>%!!#HYLTJ1JziKGr;Kq( zVH=TbgiS&Cedj~c@P572b(x0)cHY8)u@U=~wxqH`tst-6!px8rCdY>ewqcrh|AqJ4 zap2~I;2!-*(<%HGCZD5Hf~Jcr8U=ZyoWZH#YEv`x#K6rN^*gTbO#+M_-Wd2HZbzlo;G z-T**+36D)TJWU1hrkw%$>u13DaM4BrSlZ%+G@c%(``ac#I91$p5GpD#J*&J*ktL7W z0~>0)F2?e$o(W3bur4szb&rGj9|X-m4_8+XZ4u{JUB9z@!w_;Ifsh#nMm~{Bd%MG~ zx{+_bJ?eV1kz-%9iOv;SF+#lMSAnlb8_~_0o=-Xh-k)k?u;5?=i(v?T3U@vNu3XK6 zlQ2)N+uIwz1E#f6?^B>6Le%Wvyimi>y`!!vok!H6`WjHh-!)?I7tE+Ks@jXvxMA=O z!Px{cy4y#Y=|c3Ci`N2PPE@%im~IgX&k`GCC#bov%Ii31fejHtPjlEBeB9vvlTsU)ZP@-P>O&{ z8L9(O|3-|ej#Jc4TQt{#Mp#H6PGko!ZpIgH=ddr!gvO3Ni<5qOz+pvDvo#>#EYu)O ztbuReb%rLhEfdALag~9eb>odv(5$yCRlG%)gwRFQiYg)92rVDc75c+Etc=HL<2zR| zC)=tCTp#v;ky}x#jnjrawQn?BRJQ|XuiTN@udE>Gz{0zohuV#pc{vz#aFh|U!G9i} z54IU2h_csC7i7WTX3E;X;UZX;a$T!-X-;pmG6{?<0r~%K*g;HzcN4)4hk$8ET@<^v zIfs41bwquX)fjAt%%7i#l!&-i$AzN8us=;Cv!0AKaH@Kiw-?$v2rJag5Ae0zqi~5o zGnk(a)j^n+6GeXji#i*rhEG-PM%#%a1-5xNXnGkFz?OjOcd1c|Dp{ zqUuXBxU0E0f@^5JePQoQVnvbGxmfyznCYR|>uSE8u>edcuQg1-!53_4VjMI*=i6-l z)+02b0C_`Oe!t?oQJgd2ecxs`P?{wEMprekYn9(m>OsS7S*9WPc%n*Wh@^5yUZa8Z zhkR!ug!U}bpCwHVF`lV|%@w>a=D^*_=<`q&R<3)@vt=c6oP$?#bjK+i8uFi?UI zl?qp}So0IigDQ^nEYAn8s0JqP9W@^I_6;OZG1HR}nZWDu8dQ>=1pQ?sV1IPo@Taaj zz>5B-;5%b%Sa}OL87P+>aF5xq&xeP4Dx6%VXyH|U+1zDegUhzL%yMM zY*!;kXUzLzR)!VhS!iK*Ek9z{w7ZtigY~^nA1($QkWCHX`;yB_LHl zw}`TW#0HBd1xy#0Q!OlttYK_eQ9ozyJHK`wa6WXBD0NeTz~Z;hTEpm5LC>ALFRFgl zg~6}zCAxZeoKKXM`X&#=qEk_=xarQ1$6zo|=lOL|;1Op6ME)D22~2Y{Tbb?V<{ZrhOKO8or;TZF=#-luSKv{!FSvnI@^26-C;g%UbCEGufqiUj)iuJ@ zXkr$R#eo4*WxPHy7vLIWK289~`Z5r60WrpEH^p1NFRODF+}C{e8*6vX^$dOG68>Gq zW-@72W7YUkhF`ux$Oy7u`Bm>3VEkuu9$y@$nMPN^1qoHv+KG^8--5?vJDYJ9*gL`P zM5tdJsl(5?<^)^4Fx=e9G^Rh+|LjNH_P64qe6)RRdoQn|4L?hTh{r1R9;YMlY z>$dcb>Cn!nhM@{IKxAp&B?o>Fi&^)9yyRUprHgA75HVMROvRg? z5B`XVx%;&l?c$08B7%X4G?J1V=?0~9 z6VkDfZloJTq`Nzm?%s6grW-abuxZ$I$Jyxbf8O`e=QKx>!1rTX+I6SfGd33wRwPw;pN>SpTdB6@JG$Ui?&tIvuAVzgRBwe}1w|K6 z`xUCQNW-D8Z5n1At}auVk$j0WdP*&(uC1t*uJTFUFRnwozA-bG=9^)>;saUWZg&qh zl}~!afVBLt_in^rn0YjWbd1+r&)nPv5MjjTTdwh5@P(VGraN!<>GDAzy;~3K2ego_ z$zsu12FFCBFnSH z3x0lzRW?pW5!DbH1&DIz>xjZLvPXL8oXI#{cBfsS*}3Vo4Xd2%(;ecPg+Jd}z7T7R zFC@~`Fwsppf*owfHI}^bP{vn5Kv8}p3w^Hvu%{bX!ZJUugc0bDsvvxho05HnvG@3> z>}Q+n%f&Sm<>?YcE`r`Nbm0>XYZ3VqFzEJ_EyO=hZj8g*QxkGod{X2#R&HKEiz7}d zWV)ONl9L$0Y~Nie0FGEBC(p%|z-WzE=w$5XadKyaXPNI{MCGS`@KjS9jhU7cQFZxf z;BnuHGWXNFj9X=H#?|}tzym6nooNWI zKu-AuDso2c0nymV-&7dIX&0Ae9hs=-qhP`9ehG_N62;v< zNM^?UxW-{Szu=^j{k<^$t<(VWBJ1kY5JXNrHBE6G9CZg2K4}^elD|p2%H++INTSq} z{|!}bd_ib-r7yhnz5fvcLU*@pDroyKlPHRE@8atWY#%$n&x`aY4M+(1 z=ip_(+y}PJ@ZVN_!YFj_n7A@+-xLgHAPbl^ZvCnEOEV|<-9W7Xl>dUxM90XX>}nyP z7H!{XcIa1#VSs4*%Jh=W_jkHl8LLJ$%i*;gI>2}{vL-#Mbce>hv)F9vB6R}hS9)za z$+%1G9^h9!yX_(_;x+aiRB+Emx0BBGKs~q*v^rL7;kDtfeL_=9ePzvEYF~xD=VlUz>u!e-;XZ1Y{2T84 zQ%^knja_}_h#o4whyPolQJ*`R1kVyGLcY(<&==S8VKhwQ-anCyV~DZtbU)qW7A-By zWZaZ~(Cu-(`BkeLrH!Y%g*DBBn6O4M)TBOGUGYKt5r$vdUEneo{RHn0qMFY9eCozz zE63+Ll;z28)4ujCE#q4cG0o4>Uk_qGKYJC$@BQUFf{56%@F{_Zir~idV@~^({n%Hb zSv!wDE3ANZEvf62HM#GzUv_dm7@ppgx@Y>NBer%Uh@dpAv)L|bU=H%pBwge>F?hk< zPrf^d#*alu;q^x4+IyMZfZr)sg_Fm3lKh$<;0M!oWe6D18`hRB{3C=2xsG#}KQpB6 zlp~Y!q1WS@)w4FOU>Vk%1H1<*)8FnH-c1v+A2cj8{LwfSZBYK%FEvH%aGnC}psj}_cpwhKp{TkiI6 z;3_kZYne`GqB|p!9QD1-55(H;bKv{5?c!Mvh^ZtSf&EKd4wXS@O30V2!bgZ&4yBa_ zTf3zoyu_FsFVXSZxZz?HG10f9H;QQKOc58yLR1Zmsd?Xq^2Io6ST|@^EZoOa%gd=f zmZ%^xcJZvElen@g#Cf9sg@tdsiM@l4hZ9PN1U-tSbdtYe$xS$@!Us(mk;)5ii^>z` zoVoVJTAXVyfvZ|-A1m8Y=^(b{H$Z zQkBISc&aOBeU zs6vcYHnXZC*~~+exb^0ja|W7OFCp-CJ*gvtvIrn|`eATWNaVY1VdZ4_Q-G5N*`3jS ze)hSH{}OKcZn2#X*g(oK@@{jRfbc!Jot4BZ&5KX3tgR(bu?wUNFk8>@$uBQwRS06j z4zu^6QY4(!KmED#GCSA%SLJB4fQZtL6O+9=8&gPjw;3!d9!uW>9QWAHTPq=usl{X| zBf!gN%m@d7%7$H8&?<{SG;T~kF&DhUO%JDZfgZ^8bq;#C6i!YYj>X*BKFJHMX;H#N z&jlWjr;QHF9c=2^>ef|@IG?D<14OYRY^P zmU%{9G;I$^#;1e~1}wyH)c!U3PZ)0`WOhUD3tldl;b%jpj$?=3(Miu>g%J~(Z;H;W zt=#K<|5~!%v@*YNJo#MHW*fAndfU_6yTfBNl=c9@vVr-J!&QD)*loKY{bM0F0i4)U z4c6+qIj<=L&RnvmEDX^Dyb(691n*x;@(_i20^D~|4sI9iK8AmJJ>nVJ?`>TFyS(|I z|JVsal;iDed%mmtmuj}VTfH&2e%Jo}^}FJiw=PHR-2V3}Zp4cV9{l@ThX(G<=K#^` za>;+G;6*(1alHid|C!E0goeV=KI%g27nXl^-FnJX8{o-@yZ7DGxLU^T=GLJCyAES) zY~&K+-s=_lm(N8Qf%wvd2c;(8^WMQbA#s&F^34+6O{}o999L?K`X!khg*4FW_6jn3ETx9YPcOmHQ^^)y|G(DNr;Khbs%5`O*J08`*a%3Q#6q8zCx1*d@Gm-yNJ^=#6K(>31 zJTJ%#1R>CP)|AWhrUPAXK zpGDkRx+?aRWxhcjQMu=0K<$?8&e2fT5+>Y@?a@A^1OHSs9WAk-ZtM`b;qo-q zs_xAdzwTy<7Gact3$+sVW8$lKMb&oT*q;@Y`t`Mr_YJmIXx`yQT6Cr}XK8Dv+UjBU zwb6(2`MClTe#Coxh(Dz4GI`0z*Hzcs{3PC=S$|sqDoTyzq+r?NH3@UE@OC^n7Rw=V zq}BKs{c@>mP16(FJI9id6%EFIVes;g&qzMh@@RX@4x8|a^5t8Z@faOCP}8tJ*p@;| z)1F|&)I`=REF-YJTh?rgjOXVvJp z+eM9;_q^G24zdsjs}*;SdCE`c9jh#MBr@+9c897eMf2`UyuAAU%tlD=<55*+&cNW4 z9yyIOav~RFdd$t(Kw{Th9&(TB$6NeR>ZdDW%E~&-MyKIx5wkA$L@=bfz$5I7IOZck zqI=f`nbk%Yf>wf~5r5UElf1j{L7qB=syY4KlZMA;T-dRIB?$(LbBb(Yw#Ku#>`i^g z=ES>OzoE4LFcjjz8@!RugDf$Qa&<7l-NA=i#mk6oQ(SY09@CYaVeO>0Me;C?(_qB2 zSv%`NURcxMVMC9zpIqk9?;Wi+San@^!!el5i(V5u7pR_viBUz2A4Gapj z+CMrB3!$N#eV#mX;h?BelQX8e79@JhF`Yj4K0Vx<^Te*ab&}2RgW>BuwxS9b433ki z8wS_0QXK2SDA{sJPgeAjMw7=r@e}z-#n-21QyB@cUaO;Oo*)%lZj0otmyXOtseqpE zIexR!9XeM>KHPH#{pakuH$gmltDw-C9_OXepU51RaZ&c>W^?{nPYeI9eBOM%Emur9lH zi@{t01>y4JvLv_j7iLl7Jf9Tm*Q1BbpU1osb;+3`DG%l$UUThOz{)|vck2xz_{4~z ztmTAjU4E4_u{dyh7MKJ4K>1dg3|6f2HD9nNsbE(8?c2A0Q4M8}*fm|*0}`vBN?WIv zR!>B6G63cTRRcU~xJw4Oj6X95QkIMbM(Hv#^p@`v9qi7r35(O&FlJa1(1%qMFuNw< zh{bGsAIZ&0mo6vSiyyQNHjN#kUhSlIwAh{f(EEwr5?fVN|G8o&`OHfK(o=fUfTuB(V=2@*_tAJ)Yq#m>gQ|ZA+Rm z?niuj&8YSg%|@bQ5U>8+9oy&0KILf z`I%ZIdKQYN_{l-F=G!NusByICcSac{`6(3D{OlAJ-t_=`LWlUN-rlzQy2JWy#oP29 z`ddG$`($}Cbk!WLpv#vZ^-2pnZd-O#y99tN_(zmDl+C~Kzeml}^f#@AEHN2^qs*dU z_JSo=nS;itFT6jbEzbA_A8Z_bI@ucya|?L8WNREpht6&D89<)B`bn*mZl=qyKPk3U zKX1DOD>{_kQGdYNFp>>-8I?`N|Hp@ezfWdKvqF^MjdG;hZ=`po29?_2=w(N~iqqh6 z^w8}gfT~m7PWv%+N$O8|kLuNg8kRA%ReCaYK{G?IUp>C^=h5O*&<;{3XTSLF z*>Cw%*fL96#3#}I)M$N8s>&_u$@tC~r@cThe?d69U2U4t{f#$Za>n%oce z(qGq2>Sb}$t;|R%l%AJ(!wrjCV!_3Uh;^rC5q&CiFsED{fv0xx2LvHd z%A|hRQc@V>f)H+O>ueh;De#V~Qe;!%&Yg^y~O#AtKX1;6i^)xjidWC7KHh@3;O8*o-SK+89H|4ia zrr!}wH%hJf4MQ=u+o!(l{T@%18I}1FXnVb#9O-;o^EN0XnI@i{gw@EBP z#6%6umQbeiiqmcWw9mm!@iJKDr1J^#H?XIgzLtDr0!i;V@%BbvPz~$v?6qPSpTFK? zuatQ?IYFapjl4Y%^QQ4Y#n(Sf(+chz$-R;nWG(qfW5{8VtN|}p1*e`+sKHF3>4jwkaN9g4$VQ;SgG`qXkMrztbN+DqUxKd zeU57F`0i2OvOAmS81Hu4UeHpr9C~4R4fAyOj>nF zGm{|>6n?T2MBuLhU3AJ|_v}Ot@-UM>rx*wddXfR*yZ}h=QiKPY5iA)}yED<3OYZ~@ z&quI=N=IddRF`L|rY|uGiS{Rq8p{~3;zU^ZnBaze$TtU-r1fPZ^o*kb<@1H21XnpN zL7MMk?vBELt^bGUkb*I{60q@kBL$v#YWi0#1n*N5EA|k0x|S;HH;+WamuGiw4-6Jo z7HusPx^mE*$`(poQ5uf)3+Am~P0-nwTQUuBVBcIN^?R4kW_2ZeH8XRChXNw{-N;6T zOeD%a&mVP-*%kMVH@UxD(+79Zjy;%bpfJ7h={mNSD@UW1)ZEv@r(ouFK1(PFK06!5 z`y>x|$yTVo#nW3#P17*;>HMJ2V1_yI@alyIp9$%_+FlusC<7%%=;x=TtAFnqnzgVH z2PQX&bU#uH6(A0RI7s-8RB3e_7T3dm5OF@g5serD;=y-jCQ0RCrvk^M}lxzCG|DSl^ZauzU1s^&ae>?W;>Q94De66 zN&9?4pqi9PiJA^mm~Pydm|KXb!NJU}LhgHYwa>mAxzTvLnxudd0#+5mGdM5sEtNR= zVT{gvc4Vbh=6!a}1M-BeW#DOSqSjw=zERM;LNai= zNGd4rLS{}Glav<5jiFcdQ|wUx@MabkRzBdXU_;;4*5oA1H?=F(a{iq{>-fX)zZ`31 ziP)%@vmd<%kE3Ng<%1>)FNr35qfupEvRlivtoz?+ai`c#s66pYkY0k*DSSgsQSqYz zTFEJbA3wc0)9E`lIQ~Xz-lFn~o`}MrCQ@g>*ZIuIHNOAo1YnO*p zSQrlZGpQ(Lc|VqLUrN&Cm*)F&MjcGxN@qZ=5@FXqSN^<9jV2j$N4}tPrR47H5?-=t zauM!^JlFKt%*e1ysUovE{RHXTG&F2a0VK+YAHIBh_Coun$?bsOFx8ES6UqsZJDAh- zyFy?V>n@~0NfO47^a}-mA{Y8gE3LP4tw-5QOTwO z?ZOOK1=Q6ysH#T4OA`%hRFGhEU!bIv)hgFKNTmsc^2vv_;v{XUGR8aE4 zf*O$7+xEHJfXMmzxy;hhcjm1LFMAt+yyV=C$8Lr(BUxRUY@Qe%CX?Syxrm|yRlE7~ zTvivkznn6;wf6tEDg~WAbMa8yzsz{~4<49m738?@HsLpV%;lY^jnEZ3f5&piRgO{s z)*v2^mI<*^&mh|1DN{#bY&_N`zqM_4{*@=Db$R{r%ih+lp0926m($}0G9iYvDYWb; z`76E0$^(yzW$RULHX!5jger`b1qEghw}Y!*K=+)?<*Z+M;AO*CUKnI#oIgB!a-LrC zJB8iX%!If3T9lK0L2|E^MqR?F&m)m`NX=Wf3}P5!vQt1*l0B`5$2l1%7SO%c&AsYC z1$u>73%5}Pb%O(AR#o)jb(0oh5b+(JTGHx43Cf+Em){XW-4T1ax`#q4(&;C+lf$R5$utiJN#~jeDV*P9f9iR z)Y~gVaUd`@BI1Q%@kT`j3soU2>(2EQ5z9h@Q|O$&E-P$$RTK3CpX8oSpQ`eoA*CjxranX*G#5N_C)IRkG~@> z1sA$E7-hcf6*YE(N@aV{h5A;F*-xN@u-_f28;U?fz{>y(wp+6fEKM(za z^}kt+{?ln~Gqk;m-aU``hqgYJVo(2DKmWsr)SUAe@#vQs%atL^x`#6UG=JXW`UAO| ziU>;l^nP;qai+uHyiX(>2o9zQ8NBr{v6V+HBlQ2IVrtG75r7Ay*6|Y+Tqzvti=GRi ztq0xXz*&BjHbU{|tx6;lZq@}?z#SaI4lJrP<|k%w@9eJm`cJk%g+Ci+z`GB>E<5Fz zz>%)dDmcM0<-4C8cuduM`Uh(nVzdL55JMW_UNP_Y$=UhK3F3%1aXe3pKh@&Q_R*Bf zCl^besPdC}(9_Hvl{`rS!-wwE&ildm>;oG$+1Z87m$I07{rp0(`Gfu*52MZ-zw-2eQg$|PCIRO1{9pOJ6oE;BbVKvDyLKX z(?Q2fNL=rV?!h}&c)|TkFkl>GOYX$P1f`~?CK%1-&n_@Xc<%0647+I09i?#0^OjHe zD;DXS?IY$-`>DVBX2agmRB~)|G=-CslMFFEJ$>Jpf4=yeKSxhhz;Km?ot^u%10hhk z&9(tNcJ8!qx1z9%@0Wd&tiPDb+sG{K+HJ_%**`yAORnf2)xA#Qagvuw<=e?PK5uD7 zSfKuXY>czd_d^R!11tmo(O(TZBxiWOW%|MpzPrfWZk3dsOuy$K7Qk&HR9*16I-0_)=UGq z)R@;{rg&}!zRU|g-xg+iFP8E}D+n)X7+vN8<4Nmi#X#RME@wkS!^q_1nk!3$)9x(i z8Y%eL&T`ZlK>*r+0wdV_i-?YlZn5#{cm*%mMe|4nZgV`oaxpAgU=~KH<~B31yR+dE zYR-3ghM0OwmrvC4a8fX8t7+>1Vh(9tDE1Xa4hA8oQztlu9_pP$e;6`Py~ovX!%EO; zDYtgI{)x(|8jydtqRzaa;v_w>jwh@wbss^T);_u)wb7yPMa#A`XNm=$)p89j1_)%%Y6_>|L_2+95vhG$iq#p0i z9h;@jH|%sJ7I*FU{1_)Tb)pO4=?3zsrM0g@8|;6&X-I@t&brUq&Dzbu)Vk)fxeN7o z>9ykPh>ZVK{Ux+^^rY(EdShO&)4X;F?}W93+F+YYffJZmdWK#tgVA26({zW@0cyvTOLCHq_hj8yYB+r+#N)SLvKTDI_+LDi(;}p zueoYrc&WqBbDyXNF9lRmsg<kHtV<&J!QHU!E=SjFg^T<_-KYH)X&8wX+)yPIy3?gerIr%& zlp-$uxMA6tNWB`t==V7^PIMTFdYzO@RW1LewXJz;{8}{f1KlZIM+2w|{9b+DUlryCJJ}bn!DK|a!E3vPhKEp5gmd+0@lV8}ZC>hkAa)tu z)g|n6F$0n)HPRdYX=j?n9t)wDwD;JobxCa1)b`G<5#6RIiV#JME^Ne#L5H;1OiuGdVQSDfa5a*tL35e2p$BYSE3%uE z`!6@{cyX)WKE%Vv*Iye*9$8p`Z(XjeuJ(-OD!_VZi=zq(=s<2mcRZecFV~|%#h2>m z#)6?MeYMgzGYboJPT|qr?-4VBfy-0Ki{qlhSN`;em&5^1kCe5CZY_5MBNoP(P@Nx^ zId@k0`2j-7zdU~}!zg+mn94`*a+RP5}cl+p?0%mxEdBO{WcdS+b0Bv2&LQ+QO>ZHX|LJz*g^WVB_MN}48C6}>17 zS$VvBHb5SN857hY)Ii&Xomr4vD?Q0s8%Hc<2(~yFT==GGg|Td4 zlD!t;eNs0PApyb$J;-VfjikVvJ!UyWOnK zO{cJR_%DfmAp2MY6V`EX{rRv6FLjE$nutW#skR%9L`lzv0=-b5x=o5g295>P!b8^TJvh$5>N+|>gD1Cx(zu~?*5CoWd89<&h5nEJb#_T!yb1=6+> zZ5gGqMZj%@9b^! z`eP&6wTrFdSZakVF$ae#tg&Hy*iW;|zGW(d_pI50J=2N$V5+!uWaEvE2=-DHlj7p& z?W0oe*p~N8Peld1IP;Z@?Z0v+eD_R%yTM&v>^P6{(2sJ}ZlrrnD_x3BPc%qR@LibX z)Rh(Y@LyYd%_52djJ*5mb#gT-G>GQ+DrVJMUNpa9k-rnBgW}l^(L|zymD@7#rs|W= z0M)u-M$;3sUv9<=ed@O9Xt#mhyr#bj;XBT`9<*McjfW6&uOyfjl}h{6^IIA$Ox^G^ zS2D&CnTTpqHdi>n`PQBnj#3teT5o``mirR~U@(2_^CSszcF#9+6}v9*216uqQYR;9 znf80TlD^!<3&BCluA694=5SO9l(>2!EUkG2+6E& z#c@`H_l6czUMk`e>$*x@h~@5}z2ywp+y;p~l;=Qf&)keHE?s|Ddd7qppUq6H$60D7 z#YH~h>jS#*Y>y-2Oddy`if;pp#*Pk&HM@BF9*562SWLy@X;mLfX7&ESkA+~c!d9(> z;1d9s&D&x#{t*iA@+LY-(}2u!{p_)(3m>!V#WdSE?|!?)kBzbx7n}WM8D(QLo@>>h z6H6COwuS?Bs2c~Pm&}5_vWonh!=l_6@5!<5iu&&Ib>8W>xALd7=hq?EIC({O)Zqw+*l}?wWP`y*dx&>q@SBeGArMG( z3bSD*q#%=&QYA7xoD#;|oB4S}&!JrVq8-9u%xF1ZyI5~0xpDwaMMJUW;sSPC_K^;| z89IMGHrMeOeb4!MFO9AsHzt5nkuA~f#Y5YEf@DPQXSBJ0y3!Oe32Tj~@%u`5Dnh>( zH0jO=Z(pAF(k6bOB>||rgbr%U6xW|0d4$6)P5fTBLX4&u)Hm577kSZTpXM~>@lJ|I zw9e;z3^N9jMBEEi4XQu+7A)kLo`16(_}X`Dcfg+qk0Iu@uY2i>gBrqx5A(mRmtbQT zrZzMBJn(5bKoquiI{8h}$n_je^>}QxgopQY=WLlc8(j_X#_G6mxGTs=80GhB2ZHnH zGN1W;Zj|izH5nicy>*%$R3n<7pKn9VwV@K&C4<-(Qco%;JT<&*ZIysl-;!kqnj@$I zYf@7Iqtag}hNT~Tz!^X#f{w6p(Pq02U^8XD1%>P`(7UJuIW&_w#>r-?8Yxnnc2VM) zYQ!E>fk=~g3JoX+k&T&_LAYdTYNw)g03JMJMf2(_*YgPsxtkgY8+JdK|4A~6|P zL$*do$y>u|JFWhxxXI?inBTsS=arpOXM9(u#m&-qS?R7mRZ~r->MBEyzvGdJ;Vl7& zBZ(s>tE7cJQU6F+$^6CWgf?Tg_Nro;>pr#)ten)r^QeEz8E{32ihilk*Qw@+l)5V@7E0c5Xi7_XbwGJ(mhx# z_cQ{IcCP6SvJ*!JnB@-vif$lPR}zd{JjjEFt$lLw($zlJ0A ziDzCO%^@BPIG`&q4pL<>w`&T+#1a@TgniCGuc!0ILeKcIWF2qLw<}s+AD{rBe|{}A zV@(N+b;7dIi*-uMUz19;P3C$d9GNYqX8$(KwrnfNHV(=atHDKI5O@-@ytBsYIuSr}5&!`NZ<^z=qk? zelaYOzn+z&AQD3SAI<)H&$0#&S)@(jVu0i8wm*QCt+&NJvtr2a z^4`AwG9{!eS8m3S^Xj9Lqo{aQj>2o*8M57zA@X^)qJf&eWA$^=a z5k@(EB4%6_8AejCBB;I-tFLq}qjz@D_p8D6wHx@0Tf{>%SBB;`7+3<_{}!w%=|^MEwgcS33QGjO@MApo;*#cy?vs8E0qbjKUp1r%dPA!;8(vMn0-X zQ4Vhydpl<+Z$}!cX=5Y?%A&S`h`x#X;NYwh8v6;?yG zo<~XRE2qP8=X2Qs6dNRex^S|l@Wsd6(!th4_0Ni|*R$6*V*lg3eJBM442`tLe((FW z#gI0Z%k)=ahy8{rNqkmP2qxJGbs*}E%gsL4+`%ebD|XBBLP8~#-j_dSXQrwbGO#zP z;;R^7AC^MTs*k1Ldn2>4u@M{kl)bm_04mepM#*S9@zBS%x5+GJrTPY08{|=>PsKM) zGu$SuhBxh;tPJ!G5!d8FitX1Vk)K>~{hma3niTupqiZ+D{Gj#rrgY34?TGz|sG=jI zWQ%o}uT$J8OD;w=h4Xv=mDV>ke>06+N2GqP_VZ}Uw8$^miodpr5!8zpYZ^>-nSlQc zJut1bRL*{T>fQQ}?s?`BVK*E)6_V)apO%yR6Osc~2`e&mh*gKZZw1^%S!?dVb95(M zw=221|642R+8J=hq1zXuuGOcnPb_wJmIuOO8T;bUPqC|Xy|{WpWOPl7qKv-;?Ay50 zgJOHaiu{UYf=gLT^_7N^YA8;ObeQya9mCRPqfjrXa&szsOH!^)@pzV*_$v<6iMJAZ z_92+23+~Z<$2JuUNoeP_HsCA=2gzd9f+=9ZkIWmi^V3ilt;=K(<@g~W(jSVY_u)hw zTESV}9Zbk%4N{_xJ?Ffy3=~G@yM-izNGV3lZ*W|;n0iHkdr-G+E$X{nEMnvF`TorX z?yMi)Z^QZdK7R|rwZui9jX5{h{p4p$QG*&3${w}?^PD;p^Xp9{qBAo>54e11reSS! zo&up0DT;2%bVJ5d7x_&OhR z^fK#@BI`a_j^njDeiZ)QRThMg2=Zal{goOC%K3s~U|V-M!0pR2a5%`%zoN=3h({s= zGgj_nyPtg8_oEXU5sh6v%wx5r_t|PmJQhTonxF8TEs}CbGa-o^-|(G&x&7cQt)W;i z_hla*#aiY#kG&mVzQ6zm52+&8>0^l0VbYZMm(c9`mW zVfW+e+sK9P5Fz+)15UE|D2ch4 znSW$#B?F+b9&NMspqSl~*P8q-i>ZiJly-<};IWbKwsG+3JwH{e3|EVi)YVl3<7r~G zRlD~c6fI1N-F13d(mf{^phsKF4>?8X_Yq`qT%ocZc(OPGA*Uy_7@J1K5NH4_#>Lc2 zlXbi>d=1>;-Um<_j^?O7oLiJTZsnBw%9&MdQ#HnpoKL~~PPe61J5}BNBF`2jbcy`H z020BZt_8r~5?*%SOH(|CqOlD0e;tn|AVLf9OLRlC!-JYJLd{#la=$gp>yU2jhU zJq1N9=LU7h_cb-lszfMPiuA(AW8bRz2u!SJ0acla$KtEfeO7rDuCom0zL;Da+uIV< zc*N%%iWg_3xk=9)pKSHT+Z@bXO!g&))LWjQ1?7}~=icLLEtW1%lzgfIgA$;aXW=PjzxUKj-wZRuF6U z>yFs(%pD?N#$(Nj*Aw(>fx}4okY#GW%KFD8VtV@*%T*IE!#YRkLO-U<<_6dq^%EyOu~Y})CbVKnh5>*sBzb`l5Ms?{?sjY zxU-CQz6yu3eFK=az(9WoCTDnUl!?sl`X0|?KTqMdyHw_U&BI-shK44?Xhd%fdzA`k zCvK{qNWhHFv=*N>bO5XB;W#9O`T!X1Ha}9cwbZAJk{Py4X@@ipXs}~!wyUwIUJf-z zlt=e&W-CKHsy*EHhI8o$i^8$(^sQ%lX7qm=Qb`V$5^XRaYE7TQBQ@a(B>IAp1sb;z zvx>&A_6Fi#@&p%Mbiv@1Nzs>IchIDd9fNl9OMLBT~BQVm+$zl)wc~ z>FQ@otb$#a=dce_cdvg)F*b>~xpnCkpL<{G&l%KM%t=}gHj}A#1j3_cB-kW16I0|8 zd5)V24?4^0^kE|=hbVMd4IA9lsHg64EpKmF50hu~I%;uNZqB{M`Q5mLej5@-M4s3S zO)liugcJCRv^EH0G+lc5DrbY0Sp(B!cGFddHC3xpg+?>5Ju9)&qESVW?zZ^NXI+o# zN=?ivlzA2XOEDlIOjVz=%n-D=Ux1GDqc}SfQB3+W!!lA~?E$P-X>fZ4X-2x8kJ&Q7 z^?H@!V1(9l|Keg$TyS~U&)GAmTb1t}O+@mSFOB9qsQhQ1JZFnu)>qX<(=VnFd4vA( zW|@jK89BM|QQwp30{S}F+^ga>K(xozfR{Y{A-mL|rnLNN-?BW8ZdlQ%=NDc34rM*X z9cHFylHoqmTQE=Qc@YYEb^0=Wmoyv89Ht9`cb3FIyXN__=&y?k%$;P3a?uOwXU9v# ziBz$zNVBjUdUa~nJou;$Z&&X_$6l^CeL5^J>va8Lv743kHT5uL8W8_`9zP*PB=o)R z$^8!V3j9EdkH@v{#R$wSu(h709HlLaD0VxaS2vt+-D_h(N67M4|dx-`H@`D|)q+hZfs?1P-TcIq7z`2=eN?$kR!T zDi28MRxh#u)F7YtAyy&k(?MpQbG!yKt6>_QdJNxH1W(1t^`cUas{1&TYTZw&gFVn~ z<_b47l6=yJdJUDbC?1{pMyjN*pn3VL&gx(gI>x)Oo+#ndE7=rKm4u0IvWI(RU%cL+ zOPZ>^_v~%DbeYAtx%{i)H31)|OsAcI%au-5PfP853g>w+-+!X;SC2C^850*eD+GTj zAqh4Tpnk8_uOV5&kYm=Wi%|Sr4X1BA?(kx=U|@Q7D6}5?L9X&tzbszO_D$L}4IlBX(a{A%g?37gIC!Ai*wOO&#V4%z7Cw zZlSsA4|(5Yvpw+k_sWFC_zrghe@9&AB0N_lmy5`s88gJVxgszg`yC==f5|66Vuz4j-f*z{kC!1XzW@;S zrg6&X+g~j`%8)@Mk2be)asTe|9)*BF7Y=^#pI-tIpBO_(A)d@j_Ggj?A$ucYjdZ4y z{`}8AJt7I?LP(KYZm9RS?9H+F?h}H{SpI@$+;=NX2L_A$ z-C==EgcScXSii>fe+KKnsp9|4tpA$L{};@xvs->l=bu{I*e~uOe#C`kgo-|BfBj## C3KQP| diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java index c7cc34949..e685ba590 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java @@ -225,7 +225,7 @@ public enum SpServerError { * invalid. */ SP_AUTO_ASSIGN_ACTION_TYPE_INVALID("hawkbit.server.error.repo.invalidAutoAssignActionType", - "The given action type for auto-assignment is invalid: allowed values are FORCED and SOFT"), + "The given action type for auto-assignment is invalid: allowed values are ['forced', 'soft', 'downloadonly']"), /** * Error message informing that the distribution set for auto-assignment is diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpAuthenticationMessageHandler.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpAuthenticationMessageHandler.java index ee07e726a..a5e3a100a 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpAuthenticationMessageHandler.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpAuthenticationMessageHandler.java @@ -133,7 +133,6 @@ public class AmqpAuthenticationMessageHandler extends BaseAmqpService { checkByTargetId(sha1Hash, secruityToken.getTargetId()); } else { LOG.info("anonymous download no authentication check for artifact {}", sha1Hash); - return; } } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java index 7b0674c88..623f64ea1 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java @@ -270,6 +270,12 @@ public class AmqpConfiguration { return new DefaultAmqpMessageSenderService(rabbitTemplate()); } + /** + * Create RabbitListenerContainerFactory bean if no listenerContainerFactory + * bean found + * + * @return RabbitListenerContainerFactory bean + */ @Bean @ConditionalOnMissingBean(name = "listenerContainerFactory") public RabbitListenerContainerFactory listenerContainerFactory( diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java index 080c29303..8fb6f8bbb 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java @@ -38,6 +38,8 @@ import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEv import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.CancelTargetAssignmentEvent; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.ActionProperties; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; @@ -143,28 +145,28 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { PageRequest.of(0, RepositoryConstants.MAX_META_DATA_COUNT), module.getId()) .getContent())); - targetManagement.getByControllerID(assignedEvent.getActions().keySet()) - .forEach(target -> sendUpdateMessageToTarget(assignedEvent.getTenant(), target, - assignedEvent.getActions().get(target.getControllerId()), modules, - assignedEvent.isMaintenanceWindowAvailable())); + targetManagement.getByControllerID(assignedEvent.getActions().keySet()).forEach( + target -> sendUpdateMessageToTarget(assignedEvent.getActions().get(target.getControllerId()), + target, modules)); }); } /** - * Method to get the type of event depending on whether the action has a - * valid maintenance window available or not based on defined maintenance - * schedule. In case of no maintenance schedule or if there is a valid - * window available, the topic {@link EventTopic#DOWNLOAD_AND_INSTALL} is + * Method to get the type of event depending on whether the action is a + * DOWNLOAD_ONLY action or if it has a valid maintenance window available + * or not based on defined maintenance schedule. In case of no maintenance + * schedule or if there is a valid window available, the topic {@link EventTopic#DOWNLOAD_AND_INSTALL} is * returned else {@link EventTopic#DOWNLOAD} is returned. * - * @param maintenanceWindowAvailable - * valid maintenance window or not. + * @param action + * current action properties. * * @return {@link EventTopic} to use for message. */ - private static EventTopic getEventTypeForTarget(final boolean maintenanceWindowAvailable) { - return maintenanceWindowAvailable ? EventTopic.DOWNLOAD_AND_INSTALL : EventTopic.DOWNLOAD; + private static EventTopic getEventTypeForTarget(final ActionProperties action) { + return (Action.ActionType.DOWNLOAD_ONLY.equals(action.getActionType()) || + !action.isMaintenanceWindowAvailable()) ? EventTopic.DOWNLOAD : EventTopic.DOWNLOAD_AND_INSTALL; } /** @@ -206,8 +208,10 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { updateAttributesEvent.getTargetAddress()); } - protected void sendUpdateMessageToTarget(final String tenant, final Target target, final Long actionId, - final Map> modules, final boolean maintenanceWindowAvailable) { + protected void sendUpdateMessageToTarget(final ActionProperties action, final Target target, + final Map> modules) { + + String tenant = action.getTenant(); final URI targetAdress = target.getAddress(); if (!IpUtil.isAmqpUri(targetAdress)) { @@ -215,7 +219,7 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { } final DmfDownloadAndUpdateRequest downloadAndUpdateRequest = new DmfDownloadAndUpdateRequest(); - downloadAndUpdateRequest.setActionId(actionId); + downloadAndUpdateRequest.setActionId(action.getId()); final String targetSecurityToken = systemSecurityContext.runAsSystem(target::getSecurityToken); downloadAndUpdateRequest.setTargetSecurityToken(targetSecurityToken); @@ -227,8 +231,7 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { }); final Message message = getMessageConverter().toMessage(downloadAndUpdateRequest, - createConnectorMessagePropertiesEvent(tenant, target.getControllerId(), - getEventTypeForTarget(maintenanceWindowAvailable))); + createConnectorMessagePropertiesEvent(tenant, target.getControllerId(), getEventTypeForTarget(action))); amqpSenderService.sendMessage(message, targetAdress); } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java index 1cc6752c9..feccb9d7a 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java @@ -31,6 +31,7 @@ import org.eclipse.hawkbit.repository.UpdateMode; import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionProperties; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; import org.eclipse.hawkbit.repository.model.Target; @@ -218,8 +219,8 @@ public class AmqpMessageHandlerService extends BaseAmqpService { action.getDistributionSet().getModules().forEach(module -> modules.put(module, metadata.get(module.getId()))); - amqpMessageDispatcherService.sendUpdateMessageToTarget(action.getTenant(), action.getTarget(), action.getId(), - modules, action.isMaintenanceWindowAvailable()); + amqpMessageDispatcherService.sendUpdateMessageToTarget(new ActionProperties(action), action.getTarget(), + modules); } /** diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java index a289756cf..236fa43d8 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java @@ -11,7 +11,6 @@ package org.eclipse.hawkbit.amqp; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -46,6 +45,7 @@ import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionProperties; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; @@ -301,7 +301,7 @@ public class AmqpMessageHandlerServiceTest { final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); final Message message = new Message(new byte[0], messageProperties); try { - amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + amqpMessageHandlerService.onMessage(message, "unknownMessageType", TENANT, "vHost"); fail("AmqpRejectAndDontRequeueException was excepeted due to unknown message type"); } catch (final AmqpRejectAndDontRequeueException e) { } @@ -462,19 +462,16 @@ public class AmqpMessageHandlerServiceTest { // test amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); - final ArgumentCaptor tenantCaptor = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor actionPropertiesCaptor = ArgumentCaptor.forClass(ActionProperties.class); final ArgumentCaptor targetCaptor = ArgumentCaptor.forClass(Target.class); - final ArgumentCaptor actionIdCaptor = ArgumentCaptor.forClass(Long.class); - verify(amqpMessageDispatcherServiceMock, times(1)).sendUpdateMessageToTarget(tenantCaptor.capture(), - targetCaptor.capture(), actionIdCaptor.capture(), any(Map.class), anyBoolean()); - final String tenant = tenantCaptor.getValue(); - final String controllerId = targetCaptor.getValue().getControllerId(); - final Long actionId = actionIdCaptor.getValue(); - - assertThat(tenant).as("event has tenant").isEqualTo("DEFAULT"); - assertThat(controllerId).as("event has wrong controller id").isEqualTo("target1"); - assertThat(actionId).as("event has wrong action id").isEqualTo(22L); + verify(amqpMessageDispatcherServiceMock, times(1)) + .sendUpdateMessageToTarget(actionPropertiesCaptor.capture(), targetCaptor.capture(), any(Map.class)); + final ActionProperties actionProperties = actionPropertiesCaptor.getValue(); + assertThat(actionProperties).isNotNull(); + assertThat(actionProperties.getTenant()).as("event has tenant").isEqualTo("DEFAULT"); + assertThat(targetCaptor.getValue().getControllerId()).as("event has wrong controller id").isEqualTo("target1"); + assertThat(actionProperties.getId()).as("event has wrong action id").isEqualTo(22L); } @@ -516,6 +513,7 @@ public class AmqpMessageHandlerServiceTest { when(actionMock.getId()).thenReturn(targetId); when(actionMock.getTenant()).thenReturn("DEFAULT"); when(actionMock.getTarget()).thenReturn(targetMock); + when(actionMock.getActionType()).thenReturn(Action.ActionType.SOFT); when(targetMock.getControllerId()).thenReturn("target1"); return actionMock; } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java index 21d50b68f..f5b9c0c42 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java @@ -34,6 +34,7 @@ import org.eclipse.hawkbit.matcher.SoftwareModuleJsonMatcher; import org.eclipse.hawkbit.rabbitmq.test.AbstractAmqpIntegrationTest; import org.eclipse.hawkbit.rabbitmq.test.AmqpTestConfiguration; import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.SoftwareModule; @@ -235,7 +236,7 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt assertThat(targetManagement.count()).isEqualTo(expectedTargetsCount); } - private Message assertReplyMessageHeader(final EventTopic eventTopic, final String controllerId) { + protected Message assertReplyMessageHeader(final EventTopic eventTopic, final String controllerId) { verifyReplyToListener(); final Message replyMessage = replyToListener.getEventTopicMessages().get(eventTopic); assertAllTargetsCount(1); @@ -379,4 +380,16 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt return AmqpSettings.DMF_EXCHANGE; } + @Step + protected DistributionSet createTargetAndDistributionSetAndAssign(final String controllerId, + final Action.ActionType actionType) { + registerAndAssertTargetWithExistingTenant(controllerId); + + final DistributionSet distributionSet = testdataFactory.createDistributionSet(UUID.randomUUID().toString()); + testdataFactory.addSoftwareModuleMetadata(distributionSet); + + assignDistributionSet(distributionSet.getId(), controllerId, actionType); + return distributionSet; + } + } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java index be8da4674..9f177cf05 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java @@ -8,10 +8,12 @@ */ package org.eclipse.hawkbit.integration; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.Callable; +import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; @@ -25,6 +27,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedE import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.Target; @@ -32,12 +35,18 @@ import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; import org.junit.Test; +import org.mockito.Mockito; import org.springframework.amqp.core.Message; import io.qameta.allure.Description; import io.qameta.allure.Feature; import io.qameta.allure.Story; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.dmf.amqp.api.EventTopic.DOWNLOAD; +import static org.eclipse.hawkbit.dmf.amqp.api.MessageType.EVENT; +import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; + @Feature("Component Tests - Device Management Federation API") @Story("Amqp Message Dispatcher Service") public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpServiceIntegrationTest { @@ -198,6 +207,37 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer assertRequestAttributesUpdateMessage(controllerId); } + @Test + @Description("Tests the download_only assignment: asserts correct dmf Message topic, and assigned DS") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) }) + public void downloadOnlyAssignmentSendsDownloadMessageTopic() { + final String controllerId = TARGET_PREFIX + "registerTargets_1"; + final DistributionSet distributionSet = createTargetAndDistributionSetAndAssign(controllerId, DOWNLOAD_ONLY); + + // verify + final Message message = assertReplyMessageHeader(EventTopic.DOWNLOAD, controllerId); + Mockito.verifyZeroInteractions(getDeadletterListener()); + + assertThat(message).isNotNull(); + final Map headers = message.getMessageProperties().getHeaders(); + assertThat(headers).containsEntry("thingId", controllerId); + assertThat(headers).containsEntry("type", EVENT.toString()); + assertThat(headers).containsEntry("topic", DOWNLOAD.toString()); + + final Optional target = controllerManagement.getByControllerId(controllerId); + assertThat(target).isPresent(); + + // verify the DS was assigned to the Target + final DistributionSet assignedDistributionSet = ((JpaTarget) target.get()).getAssignedDistributionSet(); + assertThat(assignedDistributionSet.getId()).isEqualTo(distributionSet.getId()); + } + private void waitUntilTargetHasStatus(final String controllerId, final TargetUpdateStatus status) { waitUntil(() -> { final Optional findTargetByControllerID = targetManagement.getByControllerID(controllerId); diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java index 3716c0932..41519072f 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java @@ -9,14 +9,20 @@ package org.eclipse.hawkbit.integration; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; +import java.io.IOException; +import java.nio.charset.Charset; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.eclipse.hawkbit.amqp.AmqpProperties; import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; @@ -37,6 +43,8 @@ import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedE import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; +import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -797,6 +805,99 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic verifyNumberOfDeadLetterMessages(3); } + + @Test + @Description("Tests the download_only assignment: tests the handling of a target reporting DOWNLOADED") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionUpdatedEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetAttributesRequestedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetPollEvent.class, count = 1) }) + public void downloadOnlyAssignmentFinishesActionWhenTargetReportsDownloaded() + throws IOException { + // create target + final String controllerId = TARGET_PREFIX + "registerTargets_1"; + final DistributionSet distributionSet = createTargetAndDistributionSetAndAssign(controllerId, DOWNLOAD_ONLY); + + // verify + final Message message = assertReplyMessageHeader(EventTopic.DOWNLOAD, controllerId); + Mockito.verifyZeroInteractions(getDeadletterListener()); + + // get actionId from Message + Long actionId = Long.parseLong(getJsonFieldFromBody(message.getBody(), "actionId")); + + // Send DOWNLOADED message + sendActionUpdateStatus(new DmfActionUpdateStatus(actionId, DmfActionStatus.DOWNLOADED)); + assertAction(actionId, 1, Status.RUNNING, Status.DOWNLOADED); + Mockito.verifyZeroInteractions(getDeadletterListener()); + + verifyAssignedDsAndInstalledDs(controllerId, distributionSet.getId(), null); + } + + @Test + @Description("Tests the download_only assignment: tests the handling of a target reporting FINISHED") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionUpdatedEvent.class, count = 2), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetAttributesRequestedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 3), + @Expect(type = TargetPollEvent.class, count = 1) }) + public void downloadOnlyAssignmentAllowsActionStatusUpdatesWhenTargetReportsFinishedAndUpdatesInstalledDS() + throws IOException { + + // create target + final String controllerId = TARGET_PREFIX + "registerTargets_1"; + final DistributionSet distributionSet = createTargetAndDistributionSetAndAssign(controllerId, DOWNLOAD_ONLY); + + // verify + final Message message = assertReplyMessageHeader(EventTopic.DOWNLOAD, controllerId); + Mockito.verifyZeroInteractions(getDeadletterListener()); + + // get actionId from Message + Long actionId = Long.parseLong(getJsonFieldFromBody(message.getBody(), "actionId")); + + // Send DOWNLOADED message, should result in the action being closed + sendActionUpdateStatus(new DmfActionUpdateStatus(actionId, DmfActionStatus.DOWNLOADED)); + assertAction(actionId, 1, Status.RUNNING, Status.DOWNLOADED); + Mockito.verifyZeroInteractions(getDeadletterListener()); + + verifyAssignedDsAndInstalledDs(controllerId, distributionSet.getId(), null); + + // Send FINISHED message + sendActionUpdateStatus(new DmfActionUpdateStatus(actionId, DmfActionStatus.FINISHED)); + assertAction(actionId, 2, Status.RUNNING, Status.DOWNLOADED, Status.FINISHED); + Mockito.verifyZeroInteractions(getDeadletterListener()); + + verifyAssignedDsAndInstalledDs(controllerId, distributionSet.getId(), distributionSet.getId()); + } + + @Step + private void verifyAssignedDsAndInstalledDs(final String controllerId, final Long assignedDsId, + final Long installedDsId) { + final Optional target = controllerManagement.getByControllerId(controllerId); + assertThat(target).isPresent(); + + // verify the DS was assigned to the Target + final DistributionSet assignedDistributionSet = ((JpaTarget) target.get()).getAssignedDistributionSet(); + assertThat(assignedDsId).isNotNull(); + assertThat(assignedDistributionSet.getId()).isEqualTo(assignedDsId); + + // verify that the installed DS was not affected + final JpaDistributionSet installedDistributionSet = ((JpaTarget) target.get()).getInstalledDistributionSet(); + if (installedDsId == null) { + assertThat(installedDistributionSet).isNull(); + } else { + assertThat(installedDistributionSet.getId()).isEqualTo(installedDsId); + } + } + private void sendUpdateAttributesMessageWithGivenAttributes(final String target, final String key, final String value) { final DmfAttributeUpdate controllerAttribute = new DmfAttributeUpdate(); @@ -887,4 +988,11 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServic createConditionFactory().untilAsserted(() -> Mockito .verify(getDeadletterListener(), Mockito.times(numberOfInvocations)).handleMessage(Mockito.any())); } + + private static String getJsonFieldFromBody(final byte[] body, final String fieldName) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + final ObjectNode node = objectMapper.readValue(new String(body, Charset.defaultCharset()), ObjectNode.class); + assertThat(node.has(fieldName)).isTrue(); + return node.get(fieldName).asText(); + } } diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfActionUpdateStatus.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfActionUpdateStatus.java index ac48c002e..1b6c681e8 100644 --- a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfActionUpdateStatus.java +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfActionUpdateStatus.java @@ -34,8 +34,8 @@ public class DmfActionUpdateStatus { @JsonProperty private List message; - public DmfActionUpdateStatus(@JsonProperty(value = "actionId", required = true) Long actionId, - @JsonProperty(value = "actionStatus", required = true) DmfActionStatus actionStatus) { + public DmfActionUpdateStatus(@JsonProperty(value = "actionId", required = true) final Long actionId, + @JsonProperty(value = "actionStatus", required = true) final DmfActionStatus actionStatus) { this.actionId = actionId; this.actionStatus = actionStatus; } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java index 6579e80b9..46e563cda 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RepositoryProperties.java @@ -23,9 +23,12 @@ public class RepositoryProperties { /** * Set to true if the repository has to reject - * {@link ActionStatus} entries for actions that are closed. Note: if this - * is enforced you have to make sure that the feedback channel from the - * devices i in order. + * {@link ActionStatus} entries for actions that are closed. This is + * especially useful if the action status feedback channel order from the + * device cannot be guaranteed. + * + * Note: if this is enforced you have to make sure that the feedback + * channel from the devices is in order. */ private boolean rejectActionStatusForClosedAction; diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetAssignDistributionSetEvent.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetAssignDistributionSetEvent.java index d57742d48..a61b60770 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetAssignDistributionSetEvent.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/remote/TargetAssignDistributionSetEvent.java @@ -8,13 +8,14 @@ */ package org.eclipse.hawkbit.repository.event.remote; -import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.ActionProperties; /** * TenantAwareEvent that gets sent when a distribution set gets assigned to a @@ -28,7 +29,7 @@ public class TargetAssignDistributionSetEvent extends RemoteTenantAwareEvent { private boolean maintenanceWindowAvailable; - private final Map actions = new HashMap<>(); + private final Map actions = new HashMap<>(); /** * Default constructor. @@ -57,12 +58,20 @@ public class TargetAssignDistributionSetEvent extends RemoteTenantAwareEvent { this.distributionSetId = distributionSetId; this.maintenanceWindowAvailable = maintenanceWindowAvailable; actions.putAll(a.stream().filter(action -> action.getDistributionSet().getId().longValue() == distributionSetId) - .collect(Collectors.toMap(action -> action.getTarget().getControllerId(), Action::getId))); + .collect(Collectors.toMap(action -> action.getTarget().getControllerId(), ActionProperties::new))); } + /** + * Constructor. + * + * @param action + * the action created for this assignment + * @param applicationId + * the application id + */ public TargetAssignDistributionSetEvent(final Action action, final String applicationId) { - this(action.getTenant(), action.getDistributionSet().getId(), Arrays.asList(action), applicationId, + this(action.getTenant(), action.getDistributionSet().getId(), Collections.singletonList(action), applicationId, action.isMaintenanceWindowAvailable()); } @@ -74,7 +83,7 @@ public class TargetAssignDistributionSetEvent extends RemoteTenantAwareEvent { return maintenanceWindowAvailable; } - public Map getActions() { + public Map getActions() { return actions; } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java index 42835a0ea..996b06919 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java @@ -229,7 +229,12 @@ public interface Action extends TenantAwareBaseEntity { * {@link Action#isHitAutoForceTime(long)} is reached, {@link #FORCED} * after that. */ - TIMEFORCED; + TIMEFORCED, + + /** + * Target is only advised to download, but not install + */ + DOWNLOAD_ONLY; } /** diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionProperties.java new file mode 100644 index 000000000..489c326c7 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionProperties.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2019 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import java.io.Serializable; + +/** + * Holds properties for {@link Action} + */ +public class ActionProperties implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private Action.ActionType actionType; + private String tenant; + private boolean maintenanceWindowAvailable; + + public ActionProperties() { + } + + /** + * Constructor + * @param action + * the action to populate the properties from + */ + public ActionProperties(final Action action) { + this.id = action.getId(); + this.actionType = action.getActionType(); + this.tenant = action.getTenant(); + this.maintenanceWindowAvailable = action.isMaintenanceWindowAvailable(); + } + + public void setId(final Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setTenant(final String tenant) { + this.tenant = tenant; + } + + public String getTenant() { + return tenant; + } + + public void setMaintenanceWindowAvailable(final boolean maintenanceWindowAvailable) { + this.maintenanceWindowAvailable = maintenanceWindowAvailable; + } + + public boolean isMaintenanceWindowAvailable() { + return maintenanceWindowAvailable; + } + + public Action.ActionType getActionType() { + return actionType; + } + + public void setActionType(final Action.ActionType actionType) { + this.actionType = actionType; + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java index d8f7ab2ca..987880a33 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TargetFilterQuery.java @@ -48,7 +48,7 @@ public interface TargetFilterQuery extends TenantAwareBaseEntity { * Allowed values for auto-assign action type */ Set ALLOWED_AUTO_ASSIGN_ACTION_TYPES = Collections - .unmodifiableSet(EnumSet.of(ActionType.FORCED, ActionType.SOFT)); + .unmodifiableSet(EnumSet.of(ActionType.FORCED, ActionType.SOFT, ActionType.DOWNLOAD_ONLY)); /** * @return name of the {@link TargetFilterQuery}. diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatus.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatus.java index eed7d7bb2..340b3173c 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatus.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatus.java @@ -58,6 +58,7 @@ public class TotalTargetCountStatus { private final Map statusTotalCountMap = new EnumMap<>(Status.class); private final Long totalTargetCount; + private final Action.ActionType rolloutType; /** * Create a new states map with the target count for each state. @@ -66,11 +67,14 @@ public class TotalTargetCountStatus { * the action state map * @param totalTargetCount * the total target count + * @param rolloutType + * the type of the rollout */ public TotalTargetCountStatus(final List targetCountActionStatus, - final Long totalTargetCount) { + final Long totalTargetCount, final Action.ActionType rolloutType) { this.totalTargetCount = totalTargetCount; - mapActionStatusToTotalTargetCountStatus(targetCountActionStatus); + this.rolloutType = rolloutType; + addToTotalCount(targetCountActionStatus); } /** @@ -78,9 +82,11 @@ public class TotalTargetCountStatus { * * @param totalTargetCount * the total target count + * @param rolloutType + * the type of the rollout */ - public TotalTargetCountStatus(final Long totalTargetCount) { - this(Collections.emptyList(), totalTargetCount); + public TotalTargetCountStatus(final Long totalTargetCount, final Action.ActionType rolloutType) { + this(Collections.emptyList(), totalTargetCount, rolloutType); } /** @@ -119,8 +125,7 @@ public class TotalTargetCountStatus { * @param rolloutStatusCountItems * all target {@link Status} with total count */ - private final void mapActionStatusToTotalTargetCountStatus( - final List targetCountActionStatus) { + private void addToTotalCount(final List targetCountActionStatus) { if (targetCountActionStatus == null) { statusTotalCountMap.put(TotalTargetCountStatus.Status.NOTSTARTED, totalTargetCount); return; @@ -128,40 +133,40 @@ public class TotalTargetCountStatus { statusTotalCountMap.put(Status.RUNNING, 0L); Long notStartedTargetCount = totalTargetCount; for (final TotalTargetCountActionStatus item : targetCountActionStatus) { - convertStatus(item); + addToTotalCount(item); notStartedTargetCount -= item.getCount(); } statusTotalCountMap.put(TotalTargetCountStatus.Status.NOTSTARTED, notStartedTargetCount); } + private void addToTotalCount(final TotalTargetCountActionStatus item) { + final Status status = convertStatus(item.getStatus()); + statusTotalCountMap.merge(status, item.getCount(), Long::sum); + } + // Exception squid:MethodCyclomaticComplexity - simple state conversion, not // really complex. @SuppressWarnings("squid:MethodCyclomaticComplexity") - private void convertStatus(final TotalTargetCountActionStatus item) { - switch (item.getStatus()) { + private Status convertStatus(final Action.Status status){ + switch (status) { case SCHEDULED: - statusTotalCountMap.put(Status.SCHEDULED, item.getCount()); - break; + return Status.SCHEDULED; case ERROR: - statusTotalCountMap.put(Status.ERROR, item.getCount()); - break; + return Status.ERROR; case FINISHED: - statusTotalCountMap.put(Status.FINISHED, item.getCount()); - break; + return Status.FINISHED; + case CANCELED: + return Status.CANCELLED; case RETRIEVED: case RUNNING: case WARNING: case DOWNLOAD: - case DOWNLOADED: case CANCELING: - final Long runningItemsCount = statusTotalCountMap.get(Status.RUNNING) + item.getCount(); - statusTotalCountMap.put(Status.RUNNING, runningItemsCount); - break; - case CANCELED: - statusTotalCountMap.put(Status.CANCELLED, item.getCount()); - break; + return Status.RUNNING; + case DOWNLOADED: + return Action.ActionType.DOWNLOAD_ONLY.equals(rolloutType) ? Status.FINISHED : Status.RUNNING; default: - throw new IllegalArgumentException("State " + item.getStatus() + "is not valid"); + throw new IllegalArgumentException("State " + status + "is not valid"); } } diff --git a/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatusTest.java b/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatusTest.java new file mode 100644 index 000000000..96d9bf1ee --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/model/TotalTargetCountStatusTest.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2019 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Feature("Component Tests - TotalTargetCountStatus") +@Story("TotalTargetCountStatus should correctly present finished DOWNLOAD_ONLY actions") +public class TotalTargetCountStatusTest { + + private final List targetCountActionStatuses = Arrays.asList( + new TotalTargetCountActionStatus(Action.Status.SCHEDULED, 1L), + new TotalTargetCountActionStatus(Action.Status.ERROR, 2L), + new TotalTargetCountActionStatus(Action.Status.FINISHED, 3L), + new TotalTargetCountActionStatus(Action.Status.CANCELED, 4L), + new TotalTargetCountActionStatus(Action.Status.RETRIEVED, 5L), + new TotalTargetCountActionStatus(Action.Status.RUNNING, 6L), + new TotalTargetCountActionStatus(Action.Status.WARNING, 7L), + new TotalTargetCountActionStatus(Action.Status.DOWNLOAD, 8L), + new TotalTargetCountActionStatus(Action.Status.CANCELING, 9L), + new TotalTargetCountActionStatus(Action.Status.DOWNLOADED, 10L)); + + @Test + @Description("Different Action Statuses should be correctly mapped to the corresponding " + + "TotalTargetCountStatus.Status") + public void shouldCorrectlyMapActionStatuses() { + TotalTargetCountStatus status = new TotalTargetCountStatus(targetCountActionStatuses, 55L, + Action.ActionType.FORCED); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.SCHEDULED)).isEqualTo(1L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.ERROR)).isEqualTo(2L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.FINISHED)).isEqualTo(3L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.CANCELLED)).isEqualTo(4L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.RUNNING)).isEqualTo(45L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.NOTSTARTED)).isEqualTo(0L); + assertThat(status.getFinishedPercent()).isEqualTo((float) 100 * 3 / 55); + } + + @Test + @Description("When an empty list is passed to the TotalTargetCountStatus, all actions should be displayed as " + + "NOTSTARTED") + public void shouldCorrectlyMapActionStatusesToNotStarted() { + TotalTargetCountStatus status = new TotalTargetCountStatus(Collections.emptyList(), 55L, + Action.ActionType.FORCED); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.SCHEDULED)).isEqualTo(0L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.ERROR)).isEqualTo(0L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.FINISHED)).isEqualTo(0L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.CANCELLED)).isEqualTo(0L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.RUNNING)).isEqualTo(0L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.NOTSTARTED)).isEqualTo(55L); + assertThat(status.getFinishedPercent()).isEqualTo(0); + } + + @Test + @Description("DownloadOnly actions should be displayed as FINISHED when they have ActionStatus.DOWNLOADED") + public void shouldCorrectlyMapActionStatusesInDownloadOnlyCase() { + TotalTargetCountStatus status = new TotalTargetCountStatus(targetCountActionStatuses, 55L, + Action.ActionType.DOWNLOAD_ONLY); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.SCHEDULED)).isEqualTo(1L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.ERROR)).isEqualTo(2L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.FINISHED)).isEqualTo(13L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.CANCELLED)).isEqualTo(4L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.RUNNING)).isEqualTo(35L); + assertThat(status.getTotalTargetCountByStatus(TotalTargetCountStatus.Status.NOTSTARTED)).isEqualTo(0L); + assertThat(status.getFinishedPercent()).isEqualTo((float) 100 * 13 / 55); + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java index 8824a01dd..9abfa563e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java @@ -313,17 +313,13 @@ public interface ActionRepository extends BaseEntityRepository, * the rollout the actions are belong to * @param rolloutGroup * the rolloutgroup the actions are belong to - * @param notStatus1 - * the status the action should not have - * @param notStatus2 - * the status the action should not have - * @param notStatus3 - * the status the action should not have + * @param statuses + * the list of statuses the action should not have * @return the count of actions referring the rollout and rolloutgroup and * are not in given states */ - Long countByRolloutAndRolloutGroupAndStatusNotAndStatusNotAndStatusNot(JpaRollout rollout, - JpaRolloutGroup rolloutGroup, Status notStatus1, Status notStatus2, Status notStatus3); + Long countByRolloutAndRolloutGroupAndStatusNotIn(JpaRollout rollout, JpaRolloutGroup rolloutGroup, + List statuses); /** * Counts all actions referring to a given rollout and rolloutgroup. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java index e914c0a3c..14e088c3b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java @@ -8,6 +8,9 @@ */ package org.eclipse.hawkbit.repository.jpa; +import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; +import static org.eclipse.hawkbit.repository.model.Action.Status.DOWNLOADED; +import static org.eclipse.hawkbit.repository.model.Action.Status.FINISHED; import static org.eclipse.hawkbit.repository.model.Target.CONTROLLER_ATTRIBUTE_KEY_SIZE; import static org.eclipse.hawkbit.repository.model.Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE; @@ -452,7 +455,7 @@ public class JpaControllerManagement implements ControllerManagement { "UPDATE sp_target SET last_target_query = #last_target_query WHERE controller_id IN (" + formatQueryInStatementParams(paramMapping.keySet()) + ") AND tenant = #tenant"); - paramMapping.entrySet().forEach(entry -> updateQuery.setParameter(entry.getKey(), entry.getValue())); + paramMapping.forEach(updateQuery::setParameter); updateQuery.setParameter("last_target_query", currentTimeMillis); updateQuery.setParameter("tenant", tenant); @@ -561,23 +564,37 @@ public class JpaControllerManagement implements ControllerManagement { final JpaAction action = getActionAndThrowExceptionIfNotFound(create.getActionId()); final JpaActionStatus actionStatus = create.build(); - // if action is already closed we accept further status updates if - // permitted so by configuration. This is especially useful if the - // action status feedback channel order from the device cannot be - // guaranteed. However, if an action is closed we do not accept further - // close messages. - if (actionIsNotActiveButIntermediateFeedbackStillAllowed(actionStatus, action.isActive())) { - LOG.debug("Update of actionStatus {} for action {} not possible since action not active anymore.", - actionStatus.getStatus(), action.getId()); - return action; + if (isUpdatingActionStatusAllowed(action, actionStatus)) { + return handleAddUpdateActionStatus(actionStatus, action); } - return handleAddUpdateActionStatus(actionStatus, action); + + LOG.debug("Update of actionStatus {} for action {} not possible since action not active anymore.", + actionStatus.getStatus(), action.getId()); + return action; } - private boolean actionIsNotActiveButIntermediateFeedbackStillAllowed(final ActionStatus actionStatus, - final boolean actionActive) { - return !actionActive && (repositoryProperties.isRejectActionStatusForClosedAction() - || Status.ERROR.equals(actionStatus.getStatus()) || Status.FINISHED.equals(actionStatus.getStatus())); + /** + * ActionStatus updates are allowed mainly if the action is active. If the + * action is not active we accept further status updates if permitted so + * by repository configuration. In this case, only the values: Status.ERROR + * and Status.FINISHED are allowed. In the case of a DOWNLOAD_ONLY action, + * we accept status updates only once. + */ + private boolean isUpdatingActionStatusAllowed(final JpaAction action, final JpaActionStatus actionStatus) { + + final boolean isIntermediateFeedback = !FINISHED.equals(actionStatus.getStatus()) + && !Status.ERROR.equals(actionStatus.getStatus()); + + final boolean isAllowedByRepositoryConfiguration = !repositoryProperties.isRejectActionStatusForClosedAction() + && isIntermediateFeedback; + + final boolean isAllowedForDownloadOnlyActions = isDownloadOnly(action) && !isIntermediateFeedback; + + return action.isActive() || isAllowedByRepositoryConfiguration || isAllowedForDownloadOnlyActions; + } + + private static boolean isDownloadOnly(final JpaAction action) { + return DOWNLOAD_ONLY.equals(action.getActionType()); } /** @@ -588,6 +605,10 @@ public class JpaControllerManagement implements ControllerManagement { String controllerId = null; LOG.debug("handleAddUpdateActionStatus for action {}", action.getId()); + // information status entry - check for a potential DOS attack + assertActionStatusQuota(action); + assertActionStatusMessageQuota(actionStatus); + switch (actionStatus.getStatus()) { case ERROR: final JpaTarget target = (JpaTarget) action.getTarget(); @@ -597,10 +618,10 @@ public class JpaControllerManagement implements ControllerManagement { case FINISHED: controllerId = handleFinishedAndStoreInTargetStatus(action); break; + case DOWNLOADED: + controllerId = handleDownloadedActionStatus(action); + break; default: - // information status entry - check for a potential DOS attack - assertActionStatusQuota(action); - assertActionStatusMessageQuota(actionStatus); break; } @@ -615,6 +636,20 @@ public class JpaControllerManagement implements ControllerManagement { return savedAction; } + private String handleDownloadedActionStatus(final JpaAction action) { + if(!isDownloadOnly(action)){ + return null; + } + + JpaTarget target = (JpaTarget) action.getTarget(); + action.setActive(false); + action.setStatus(DOWNLOADED); + target.setUpdateStatus(TargetUpdateStatus.IN_SYNC); + targetRepository.save(target); + + return target.getControllerId(); + } + private void requestControllerAttributes(final String controllerId) { final JpaTarget target = (JpaTarget) getByControllerId(controllerId) .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); @@ -648,6 +683,12 @@ public class JpaControllerManagement implements ControllerManagement { target.setInstalledDistributionSet(ds); target.setInstallationDate(System.currentTimeMillis()); + // Target reported an installation of a DOWNLOAD_ONLY assignment, the assigned DS has to be adapted + // because the currently assigned DS can be unequal to the currently installed DS (the downloadOnly DS) + if(isDownloadOnly(action)){ + target.setAssignedDistributionSet(action.getDistributionSet()); + } + // check if the assigned set is equal to the installed set (not // necessarily the case as another update might be pending already). if (target.getAssignedDistributionSet() != null diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java index 0b9bc171e..4ed285608 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java @@ -151,7 +151,8 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement { for (final JpaRolloutGroup rolloutGroup : rolloutGroups) { final TotalTargetCountStatus totalTargetCountStatus = new TotalTargetCountStatus( - allStatesForRollout.get(rolloutGroup.getId()), Long.valueOf(rolloutGroup.getTotalTargets())); + allStatesForRollout.get(rolloutGroup.getId()), Long.valueOf(rolloutGroup.getTotalTargets()), + rolloutGroup.getRollout().getActionType()); rolloutGroup.setTotalTargetCountStatus(totalTargetCountStatus); } @@ -177,7 +178,7 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement { } final TotalTargetCountStatus totalTargetCountStatus = new TotalTargetCountStatus(rolloutStatusCountItems, - Long.valueOf(jpaRolloutGroup.getTotalTargets())); + Long.valueOf(jpaRolloutGroup.getTotalTargets()), jpaRolloutGroup.getRollout().getActionType()); jpaRolloutGroup.setTotalTargetCountStatus(totalTargetCountStatus); return rolloutGroup; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java index 15e8c5296..064af9c2a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java @@ -105,6 +105,8 @@ import org.springframework.validation.annotation.Validated; import com.google.common.collect.Lists; +import static org.eclipse.hawkbit.repository.jpa.builder.JpaRolloutGroupCreate.addSuccessAndErrorConditionsAndActions; + /** * JPA implementation of {@link RolloutManagement}. */ @@ -126,6 +128,12 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { private static final List ACTIVE_ROLLOUTS = Arrays.asList(RolloutStatus.CREATING, RolloutStatus.DELETING, RolloutStatus.STARTING, RolloutStatus.READY, RolloutStatus.RUNNING); + // In case of DOWNLOAD_ONLY, actions can be finished with DOWNLOADED status. + private static final List DOWNLOAD_ONLY_ACTION_TERMINATION_STATUSES = Arrays.asList(Status.ERROR, + Status.FINISHED, Status.CANCELED, Status.DOWNLOADED); + private static final List DEFAULT_ACTION_TERMINATION_STATUSES = Arrays.asList(Status.ERROR, Status.FINISHED, + Status.CANCELED); + @Autowired private RolloutRepository rolloutRepository; @@ -251,17 +259,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { group.setParent(lastSavedGroup); group.setStatus(RolloutGroupStatus.CREATING); - group.setSuccessCondition(conditions.getSuccessCondition()); - group.setSuccessConditionExp(conditions.getSuccessConditionExp()); - - group.setSuccessAction(conditions.getSuccessAction()); - group.setSuccessActionExp(conditions.getSuccessActionExp()); - - group.setErrorCondition(conditions.getErrorCondition()); - group.setErrorConditionExp(conditions.getErrorConditionExp()); - - group.setErrorAction(conditions.getErrorAction()); - group.setErrorActionExp(conditions.getErrorActionExp()); + addSuccessAndErrorConditionsAndActions(group, conditions); group.setTargetPercentage(1.0F / (amountOfGroups - i) * 100); @@ -310,17 +308,10 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { group.setTargetFilterQuery(""); } - group.setSuccessCondition(srcGroup.getSuccessCondition()); - group.setSuccessConditionExp(srcGroup.getSuccessConditionExp()); - - group.setSuccessAction(srcGroup.getSuccessAction()); - group.setSuccessActionExp(srcGroup.getSuccessActionExp()); - - group.setErrorCondition(srcGroup.getErrorCondition()); - group.setErrorConditionExp(srcGroup.getErrorConditionExp()); - - group.setErrorAction(srcGroup.getErrorAction()); - group.setErrorActionExp(srcGroup.getErrorActionExp()); + addSuccessAndErrorConditionsAndActions(group, srcGroup.getSuccessCondition(), + srcGroup.getSuccessConditionExp(), srcGroup.getSuccessAction(), srcGroup.getSuccessActionExp(), + srcGroup.getErrorCondition(), srcGroup.getErrorConditionExp(), srcGroup.getErrorAction(), + srcGroup.getErrorActionExp()); lastSavedGroup = rolloutGroupRepository.save(group); publishRolloutGroupCreatedEventAfterCommit(lastSavedGroup, rollout); @@ -760,9 +751,11 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } private boolean isRolloutGroupComplete(final JpaRollout rollout, final JpaRolloutGroup rolloutGroup) { - final Long actionsLeftForRollout = actionRepository - .countByRolloutAndRolloutGroupAndStatusNotAndStatusNotAndStatusNot(rollout, rolloutGroup, - Action.Status.ERROR, Action.Status.FINISHED, Action.Status.CANCELED); + final Long actionsLeftForRollout = ActionType.DOWNLOAD_ONLY.equals(rollout.getActionType()) + ? actionRepository.countByRolloutAndRolloutGroupAndStatusNotIn(rollout, rolloutGroup, + DOWNLOAD_ONLY_ACTION_TERMINATION_STATUSES) + : actionRepository.countByRolloutAndRolloutGroupAndStatusNotIn(rollout, rolloutGroup, + DEFAULT_ACTION_TERMINATION_STATUSES); return actionsLeftForRollout == 0; } @@ -1070,7 +1063,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } final TotalTargetCountStatus totalTargetCountStatus = new TotalTargetCountStatus(rolloutStatusCountItems, - rollout.get().getTotalTargets()); + rollout.get().getTotalTargets(), rollout.get().getActionType()); ((JpaRollout) rollout.get()).setTotalTargetCountStatus(totalTargetCountStatus); return rollout; } @@ -1112,7 +1105,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { if (allStatesForRollout != null) { rollouts.forEach(rollout -> { final TotalTargetCountStatus totalTargetCountStatus = new TotalTargetCountStatus( - allStatesForRollout.get(rollout.getId()), rollout.getTotalTargets()); + allStatesForRollout.get(rollout.getId()), rollout.getTotalTargets(), rollout.getActionType()); rollout.setTotalTargetCountStatus(totalTargetCountStatus); }); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutGroupCreate.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutGroupCreate.java index 66e7f1610..50c861731 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutGroupCreate.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutGroupCreate.java @@ -11,6 +11,8 @@ package org.eclipse.hawkbit.repository.jpa.builder; import org.eclipse.hawkbit.repository.builder.AbstractRolloutGroupCreate; import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; +import org.eclipse.hawkbit.repository.model.RolloutGroup; +import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; public class JpaRolloutGroupCreate extends AbstractRolloutGroupCreate implements RolloutGroupCreate { @@ -30,20 +32,66 @@ public class JpaRolloutGroupCreate extends AbstractRolloutGroupCreate entry = new SimpleImmutableEntry(action.getTarget().getControllerId(), - action.getId()); - - assertThat(underTest.getActions()).containsExactly(entry); + assertThat(underTest.getActions().size()).isEqualTo(1); + ActionProperties actionProperties = underTest.getActions().get(action.getTarget().getControllerId()); + assertThat(actionProperties).isNotNull(); + assertThat(actionProperties).isEqualToComparingFieldByField(new ActionProperties(action)); assertThat(underTest.getDistributionSetId()).isEqualTo(action.getDistributionSet().getId()); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java index 7cf48859e..cbbac057e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java @@ -11,6 +11,8 @@ package org.eclipse.hawkbit.repository.jpa; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS; +import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; +import static org.eclipse.hawkbit.repository.test.util.TestdataFactory.DEFAULT_CONTROLLER_ID; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; @@ -20,6 +22,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -42,19 +45,20 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.ArtifactUpload; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; -import org.eclipse.hawkbit.repository.test.util.TestdataFactory; import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -148,7 +152,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.FINISHED, Action.Status.FINISHED, false); assertThat(actionStatusRepository.count()).isEqualTo(7); @@ -199,7 +203,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.FINISHED, Action.Status.FINISHED, false); assertThat(actionStatusRepository.count()).isEqualTo(3); @@ -224,7 +228,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { // expected } - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.RUNNING, true); assertThat(actionStatusRepository.count()).isEqualTo(1); @@ -245,14 +249,14 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.CANCELING, true); simulateIntermediateStatusOnCancellation(actionId); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.CANCELED, Action.Status.FINISHED, false); assertThat(actionStatusRepository.count()).isEqualTo(8); @@ -272,14 +276,14 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.CANCELING, true); simulateIntermediateStatusOnCancellation(actionId); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.CANCELED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.CANCELED, Action.Status.CANCELED, false); assertThat(actionStatusRepository.count()).isEqualTo(8); @@ -300,14 +304,14 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.CANCELING, true); simulateIntermediateStatusOnCancellation(actionId); controllerManagement.addCancelActionStatus( entityFactory.actionStatus().create(actionId).status(Action.Status.CANCEL_REJECTED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.CANCEL_REJECTED, true); assertThat(actionStatusRepository.count()).isEqualTo(8); @@ -328,14 +332,14 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final Long actionId = createTargetAndAssignDs(); deploymentManagement.cancelAction(actionId); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.CANCELING, true); simulateIntermediateStatusOnCancellation(actionId); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.ERROR)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.ERROR, true); assertThat(actionStatusRepository.count()).isEqualTo(8); @@ -346,39 +350,64 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { private Long createTargetAndAssignDs() { final Long dsId = testdataFactory.createDistributionSet().getId(); testdataFactory.createTarget(); - assignDistributionSet(dsId, TestdataFactory.DEFAULT_CONTROLLER_ID); - assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) + assignDistributionSet(dsId, DEFAULT_CONTROLLER_ID); + assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.PENDING); - return deploymentManagement.findActiveActionsByTarget(PAGE, TestdataFactory.DEFAULT_CONTROLLER_ID).getContent() + return deploymentManagement.findActiveActionsByTarget(PAGE, DEFAULT_CONTROLLER_ID).getContent() .get(0).getId(); } + @Step + private Long createAndAssignDsAsDownloadOnly(final String dsName, final String defaultControllerId) { + final Long dsId = testdataFactory.createDistributionSet(dsName).getId(); + assignDistributionSet(dsId, defaultControllerId, DOWNLOAD_ONLY); + assertThat(targetManagement.getByControllerID(defaultControllerId).get().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + + final Long id = deploymentManagement.findActiveActionsByTarget(PAGE, defaultControllerId).getContent() + .get(0).getId(); + assertThat(id).isNotNull(); + return id; + } + + @Step + private Long assignDs(final Long dsId, final String defaultControllerId, final Action.ActionType actionType) { + assignDistributionSet(dsId, defaultControllerId, actionType); + assertThat(targetManagement.getByControllerID(defaultControllerId).get().getUpdateStatus()) + .isEqualTo(TargetUpdateStatus.PENDING); + + final Long id = deploymentManagement.findActiveActionsByTarget(PAGE, defaultControllerId).getContent() + .get(0).getId(); + assertThat(id).isNotNull(); + return id; + } + @Step private void simulateIntermediateStatusOnCancellation(final Long actionId) { controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.RUNNING)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.RUNNING, true); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.DOWNLOAD)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.DOWNLOAD, true); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.DOWNLOADED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.DOWNLOADED, true); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.RETRIEVED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.RETRIEVED, true); controllerManagement .addCancelActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.WARNING)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.CANCELING, Action.Status.WARNING, true); } @@ -386,26 +415,26 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { private void simulateIntermediateStatusOnUpdate(final Long actionId) { controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.RUNNING)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.RUNNING, true); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.DOWNLOAD)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.DOWNLOAD, true); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.DOWNLOADED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.DOWNLOADED, true); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.RETRIEVED)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.RETRIEVED, true); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.WARNING)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.WARNING, true); } @@ -509,7 +538,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Controller trys to finish an update process after it has been finished by an error action status.") + @Description("Controller tries to finish an update process after it has been finished by an error action status.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = DistributionSetCreatedEvent.class, count = 1), @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1), @@ -522,12 +551,12 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { // test and verify controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.RUNNING)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, Action.Status.RUNNING, Action.Status.RUNNING, true); controllerManagement .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.ERROR)); - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, Action.Status.ERROR, Action.Status.ERROR, false); // try with disabled late feedback @@ -536,7 +565,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); // test - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, Action.Status.ERROR, Action.Status.ERROR, false); // try with enabled late feedback - should not make a difference as it @@ -546,7 +575,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); // test - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.ERROR, Action.Status.ERROR, Action.Status.ERROR, false); assertThat(actionStatusRepository.count()).isEqualTo(3); @@ -572,7 +601,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); // test - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.FINISHED, Action.Status.FINISHED, false); // try with enabled late feedback - should not make a difference as it @@ -582,7 +611,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Action.Status.FINISHED)); // test - assertActionStatus(actionId, TestdataFactory.DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, Action.Status.FINISHED, Action.Status.FINISHED, false); assertThat(actionStatusRepository.count()).isEqualTo(3); @@ -609,7 +638,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { entityFactory.actionStatus().create(action.getId()).status(Action.Status.RUNNING)); // nothing changed as "feedback after close" is disabled - assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) + assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.IN_SYNC); assertThat(actionStatusRepository.count()).isEqualTo(3); @@ -635,7 +664,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { entityFactory.actionStatus().create(action.getId()).status(Action.Status.RUNNING)); // nothing changed as "feedback after close" is disabled - assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) + assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.IN_SYNC); // however, additional action status has been stored @@ -989,4 +1018,312 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } + @Test + @Description("Verifies that a DOWNLOAD_ONLY action is not marked complete when the controller reports DOWNLOAD") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void controllerReportsDownloadForDownloadOnlyAction() { + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOAD)); + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.PENDING, + Action.Status.RUNNING, Action.Status.DOWNLOAD, true); + + assertThat(actionStatusRepository.count()).isEqualTo(2); + assertThat(controllerManagement.findActionStatusByAction(PAGE, actionId).getNumberOfElements()).isEqualTo(2); + assertThat(actionRepository.activeActionExistsForControllerId(DEFAULT_CONTROLLER_ID)) + .isEqualTo(true); + } + + @Test + @Description("Verifies that a DOWNLOAD_ONLY action is marked complete once the controller reports DOWNLOADED") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetAttributesRequestedEvent.class, count = 1), + @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void controllerReportsDownloadedForDownloadOnlyAction() { + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED)); + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + Action.Status.DOWNLOADED, Action.Status.DOWNLOADED, false); + + assertThat(actionStatusRepository.count()).isEqualTo(2); + assertThat(controllerManagement.findActionStatusByAction(PAGE, actionId).getNumberOfElements()).isEqualTo(2); + assertThat(actionRepository.activeActionExistsForControllerId(DEFAULT_CONTROLLER_ID)) + .isEqualTo(false); + } + + @Test + @Description("Verifies that a controller can report a FINISHED event for a DOWNLOAD_ONLY non-active action.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 3), + @Expect(type = TargetAttributesRequestedEvent.class, count = 2), + @Expect(type = ActionUpdatedEvent.class, count = 2), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void controllerReportsActionFinishedForDownloadOnlyActionThatIsNotActive() { + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + finishDownloadOnlyUpdateAndSendUpdateActionStatus(actionId, Status.FINISHED); + + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + Action.Status.FINISHED, Action.Status.FINISHED, false); + + assertThat(actionStatusRepository.count()).isEqualTo(3); + assertThat(controllerManagement.findActionStatusByAction(PAGE, actionId).getNumberOfElements()).isEqualTo(3); + assertThat(actionRepository.activeActionExistsForControllerId(DEFAULT_CONTROLLER_ID)) + .isEqualTo(false); + } + + @Test + @Description("Verifies that multiple DOWNLOADED events for a DOWNLOAD_ONLY action are handled.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetAttributesRequestedEvent.class, count = 3), + @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void controllerReportsMultipleDownloadedForDownloadOnlyAction() { + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + IntStream.range(0, 3).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED))); + + assertActionStatus(actionId, DEFAULT_CONTROLLER_ID, TargetUpdateStatus.IN_SYNC, + Status.DOWNLOADED, Status.DOWNLOADED, false); + + assertThat(actionStatusRepository.count()).isEqualTo(4); + assertThat(controllerManagement.findActionStatusByAction(PAGE, actionId).getNumberOfElements()).isEqualTo(4); + assertThat(actionRepository.activeActionExistsForControllerId(DEFAULT_CONTROLLER_ID)) + .isEqualTo(false); + } + + @Test(expected = QuotaExceededException.class) + @Description("Verifies that quota is asserted when a controller reports too many DOWNLOADED events for a " + + "DOWNLOAD_ONLY action.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = TargetAttributesRequestedEvent.class, count = 9), + @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void quotaExceptionWhencontrollerReportsTooManyDownloadedMessagesForDownloadOnlyAction() { + final int maxMessages = quotaManagement.getMaxMessagesPerActionStatus(); + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + + IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED))); + } + + @Test + @Description("Verifies that quota is enforced for UpdateActionStatus events for DOWNLOAD_ONLY assignments.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetAttributesRequestedEvent.class, count = 9), + @Expect(type = ActionUpdatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void quotaEceededExceptionWhenControllerReportsTooManyUpdateActionStatusMessagesForDownloadOnlyAction() { + final int maxMessages = quotaManagement.getMaxMessagesPerActionStatus(); + testdataFactory.createTarget(); + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + + try { + IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED))); + fail("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates"); + } catch (QuotaExceededException e) { } + + try { + IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.ERROR))); + fail("No QuotaExceededException thrown for too many ERROR updateActionStatus updates"); + } catch (QuotaExceededException e) { } + + try { + IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.FINISHED))); + fail("No QuotaExceededException thrown for too many FINISHED updateActionStatus updates"); + } catch (QuotaExceededException e) { } + } + + @Test + @Description("Verifies that quota is enforced for UpdateActionStatus events for FORCED assignments.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void quotaEceededExceptionWhenControllerReportsTooManyUpdateActionStatusMessagesForForced() { + final int maxMessages = quotaManagement.getMaxMessagesPerActionStatus(); + final Long actionId = createTargetAndAssignDs(); + assertThat(actionId).isNotNull(); + + try { + IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.DOWNLOADED))); + fail("No QuotaExceededException thrown for too many DOWNLOADED updateActionStatus updates"); + } catch (QuotaExceededException e) { } + + try { + IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.ERROR))); + fail("No QuotaExceededException thrown for too many ERROR updateActionStatus updates"); + } catch (QuotaExceededException e) { } + + try { + IntStream.range(0, maxMessages).forEach(i -> controllerManagement + .addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(Status.FINISHED))); + fail("No QuotaExceededException thrown for too many FINISHED updateActionStatus updates"); + } catch (QuotaExceededException e) { } + } + + @Test + @Description("Verifies that a target can report FINISHED/ERROR updates for DOWNLOAD_ONLY assignments regardless of " + + "repositoryProperties.rejectActionStatusForClosedAction value.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 4), + @Expect(type = ActionCreatedEvent.class, count = 4), + @Expect(type = TargetUpdatedEvent.class, count = 12), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 4), + @Expect(type = TargetAttributesRequestedEvent.class, count = 6), + @Expect(type = ActionUpdatedEvent.class, count = 8), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 12) }) + public void targetCanAlwaysReportFinishedOrErrorAfterActionIsClosedForDownloadOnlyAssignments() { + + testdataFactory.createTarget(); + + // allow actionStatusUpdates for closed actions + repositoryProperties.setRejectActionStatusForClosedAction(false); + + final Long actionId = createAndAssignDsAsDownloadOnly("downloadOnlyDs1", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + finishDownloadOnlyUpdateAndSendUpdateActionStatus(actionId, Status.FINISHED); + + final Long actionId2 = createAndAssignDsAsDownloadOnly("downloadOnlyDs2", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + finishDownloadOnlyUpdateAndSendUpdateActionStatus(actionId2, Status.ERROR); + + // disallow actionStatusUpdates for closed actions + repositoryProperties.setRejectActionStatusForClosedAction(true); + + final Long actionId3 = createAndAssignDsAsDownloadOnly("downloadOnlyDs3", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + finishDownloadOnlyUpdateAndSendUpdateActionStatus(actionId3, Status.FINISHED); + + + final Long actionId4 = createAndAssignDsAsDownloadOnly("downloadOnlyDs4", DEFAULT_CONTROLLER_ID); + assertThat(actionId).isNotNull(); + finishDownloadOnlyUpdateAndSendUpdateActionStatus(actionId4, Status.ERROR); + + // actionStatusRepository should have 12 ActionStatusUpdates, 3 from each action + assertThat(actionStatusRepository.count()).isEqualTo(12L); + } + + @Step + private void finishDownloadOnlyUpdateAndSendUpdateActionStatus(final Long actionId, final Status status) { + // finishing action + controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId) + .status(Status.DOWNLOADED)); + + controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId) + .status(status)); + assertThat(actionRepository.activeActionExistsForControllerId(DEFAULT_CONTROLLER_ID)) + .isEqualTo(false); + } + + @Test + @Description("Verifies that a controller can report a FINISHED event for a DOWNLOAD_ONLY action after having" + + " installed an intermediate update.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), + @Expect(type = ActionCreatedEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 5), + @Expect(type = TargetAttributesRequestedEvent.class, count = 3), + @Expect(type = ActionUpdatedEvent.class, count = 3), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6) }) + public void controllerReportsFinishedForOldDownloadOnlyActionAfterSuccessfulForcedAssignment() { + + testdataFactory.createTarget(); + final DistributionSet downloadOnlyDs = testdataFactory.createDistributionSet("downloadOnlyDs1"); + + // assign DOWNLOAD_ONLY Distribution set + final Long downloadOnlyActionId = assignDs(downloadOnlyDs.getId(), DEFAULT_CONTROLLER_ID, DOWNLOAD_ONLY); + addUpdateActionStatus(downloadOnlyActionId, DEFAULT_CONTROLLER_ID, Status.DOWNLOADED); + assertAssignedDistributionSetId(DEFAULT_CONTROLLER_ID, downloadOnlyDs.getId()); + assertInstalledDistributionSetId(DEFAULT_CONTROLLER_ID, null); + assertNoActiveActionsExistsForControllerId(DEFAULT_CONTROLLER_ID); + + // assign distributionSet as FORCED assignment + final Long forcedDistributionSetId = testdataFactory.createDistributionSet("forcedDs1").getId(); + final DistributionSetAssignmentResult assignmentResult = + assignDistributionSet(forcedDistributionSetId, DEFAULT_CONTROLLER_ID, Action.ActionType.SOFT); + addUpdateActionStatus(assignmentResult.getActions().get(0), DEFAULT_CONTROLLER_ID, Status.FINISHED); + assertAssignedDistributionSetId(DEFAULT_CONTROLLER_ID, forcedDistributionSetId); + assertInstalledDistributionSetId(DEFAULT_CONTROLLER_ID, forcedDistributionSetId); + assertNoActiveActionsExistsForControllerId(DEFAULT_CONTROLLER_ID); + + // report FINISHED for the DOWNLOAD_ONLY action + addUpdateActionStatus(downloadOnlyActionId, DEFAULT_CONTROLLER_ID, Status.FINISHED); + assertAssignedDistributionSetId(DEFAULT_CONTROLLER_ID, downloadOnlyDs.getId()); + assertInstalledDistributionSetId(DEFAULT_CONTROLLER_ID, downloadOnlyDs.getId()); + assertNoActiveActionsExistsForControllerId(DEFAULT_CONTROLLER_ID); + } + + @Step + private void addUpdateActionStatus(final Long actionId, final String controllerId, final Status actionStatus) { + controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId).status(actionStatus)); + assertActionStatus(actionId, controllerId, TargetUpdateStatus.IN_SYNC, actionStatus, actionStatus, false); + } + + private void assertAssignedDistributionSetId(final String controllerId, final Long dsId) { + final Optional target = controllerManagement.getByControllerId(controllerId); + assertThat(target).isPresent(); + final DistributionSet assignedDistributionSet = ((JpaTarget) target.get()).getAssignedDistributionSet(); + assertThat(assignedDistributionSet.getId()).isEqualTo(dsId); + } + + private void assertInstalledDistributionSetId(final String controllerId, final Long dsId) { + final Optional target = controllerManagement.getByControllerId(controllerId); + assertThat(target).isPresent(); + final DistributionSet installedDistributionSet = ((JpaTarget) target.get()).getInstalledDistributionSet(); + if(dsId == null){ + assertThat(installedDistributionSet).isNull(); + } else { + assertThat(installedDistributionSet.getId()).isEqualTo(dsId); + } + } + + private void assertNoActiveActionsExistsForControllerId(final String controllerId) { + assertThat(actionRepository.activeActionExistsForControllerId(controllerId)).isEqualTo(false); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java index ddb336406..58de216b4 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java @@ -47,6 +47,7 @@ import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; +import org.eclipse.hawkbit.repository.model.ActionProperties; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; @@ -1086,11 +1087,15 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { assertThat(event).isNotNull(); assertThat(event.getDistributionSetId()).isEqualTo(ds.getId()); - assertThat(event.getActions()).isEqualTo(targets.stream() - .map(target -> deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()) - .getContent()) + List eventActionIds = event.getActions().values().stream().map(ActionProperties::getId) + .collect(Collectors.toList()); + + List targetActiveActionIds = targets.stream() + .map(t -> deploymentManagement.findActiveActionsByTarget(PAGE, t.getControllerId()).getContent()) .flatMap(List::stream) - .collect(Collectors.toMap(action -> action.getTarget().getControllerId(), Action::getId))); + .map(Action::getId) + .collect(Collectors.toList()); + assertThat(eventActionIds).containsOnlyElementsOf(targetActiveActionIds); } private class DeploymentResult { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java index 1e91f5051..7eb116df3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java @@ -590,6 +590,69 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { } + @Test + @Description("Verify that the targets have the right status during a download_only rollout.") + public void countCorrectStatusForEachTargetDuringDownloadOnlyRollout() { + + final int amountTargetsForRollout = 8; + final int amountOtherTargets = 15; + final int amountGroups = 4; + final String successCondition = "50"; + final String errorCondition = "80"; + final Rollout createdRollout = createSimpleTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout, + amountOtherTargets, amountGroups, successCondition, errorCondition, ActionType.DOWNLOAD_ONLY); + + // targets have not started + Map validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.NOTSTARTED, 8L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + rolloutManagement.start(createdRollout.getId()); + + // Run here, because scheduler is disabled during tests + rolloutManagement.handleRollouts(); + + // 6 targets are ready and 2 are running + validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.SCHEDULED, 6L); + validationMap.put(TotalTargetCountStatus.Status.RUNNING, 2L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + changeStatusForAllRunningActions(createdRollout, Status.DOWNLOADED); + rolloutManagement.handleRollouts(); + // 4 targets are ready, 2 are finished(with DOWNLOADED action status) and 2 are running + validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.SCHEDULED, 4L); + validationMap.put(TotalTargetCountStatus.Status.FINISHED, 2L); + validationMap.put(TotalTargetCountStatus.Status.RUNNING, 2L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + changeStatusForAllRunningActions(createdRollout, Status.DOWNLOADED); + rolloutManagement.handleRollouts(); + // 2 targets are ready, 4 are finished(with DOWNLOADED action status) and 2 are running + validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.SCHEDULED, 2L); + validationMap.put(TotalTargetCountStatus.Status.FINISHED, 4L); + validationMap.put(TotalTargetCountStatus.Status.RUNNING, 2L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + changeStatusForAllRunningActions(createdRollout, Status.DOWNLOADED); + rolloutManagement.handleRollouts(); + // 0 targets are ready, 6 are finished(with DOWNLOADED action status) and 2 are running + validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.FINISHED, 6L); + validationMap.put(TotalTargetCountStatus.Status.RUNNING, 2L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + changeStatusForAllRunningActions(createdRollout, Status.FINISHED); + rolloutManagement.handleRollouts(); + // 0 targets are ready, 6 are finished(with DOWNLOADED action status), 2 are finished and 0 are running + validationMap = createInitStatusMap(); + validationMap.put(TotalTargetCountStatus.Status.FINISHED, 8L); + validateRolloutActionStatus(createdRollout.getId(), validationMap); + + } + @Test @Description("Verify that the targets have the right status during the rollout when an error emerges.") public void countCorrectStatusForEachTargetDuringRolloutWithError() { @@ -1727,12 +1790,19 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { private Rollout createSimpleTestRolloutWithTargetsAndDistributionSet(final int amountTargetsForRollout, final int amountOtherTargets, final int groupSize, final String successCondition, final String errorCondition) { + return createSimpleTestRolloutWithTargetsAndDistributionSet(amountTargetsForRollout, amountOtherTargets, + groupSize, successCondition, errorCondition, ActionType.FORCED); + } + + private Rollout createSimpleTestRolloutWithTargetsAndDistributionSet(final int amountTargetsForRollout, + final int amountOtherTargets, final int groupSize, final String successCondition, + final String errorCondition, final ActionType actionType) { final DistributionSet rolloutDS = testdataFactory.createDistributionSet("rolloutDS"); testdataFactory.createTargets(amountTargetsForRollout, "rollout-", "rollout"); testdataFactory.createTargets(amountOtherTargets, "others-", "rollout"); final String filterQuery = "controllerId==rollout-*"; return testdataFactory.createRolloutByVariables("test-rollout-name-1", "test-rollout-description-1", groupSize, - filterQuery, rolloutDS, successCondition, errorCondition); + filterQuery, rolloutDS, successCondition, errorCondition, actionType); } private Rollout createTestRolloutWithTargetsAndDistributionSet(final int amountTargetsForRollout, diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java index b50362434..6ee867a96 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java @@ -196,6 +196,8 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest verifyAutoAssignmentWithSoftActionType(filterName, targetFilterQuery, distributionSet); + verifyAutoAssignmentWithDownloadOnlyActionType(filterName, targetFilterQuery, distributionSet); + verifyAutoAssignmentWithInvalidActionType(targetFilterQuery, distributionSet); verifyAutoAssignmentWithIncompleteDs(targetFilterQuery); @@ -218,6 +220,14 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest verifyAutoAssignDsAndActionType(filterName, distributionSet, ActionType.SOFT); } + @Step + private void verifyAutoAssignmentWithDownloadOnlyActionType(final String filterName, + final TargetFilterQuery targetFilterQuery, final DistributionSet distributionSet) { + targetFilterQueryManagement.updateAutoAssignDSWithActionType(targetFilterQuery.getId(), distributionSet.getId(), + ActionType.DOWNLOAD_ONLY); + verifyAutoAssignDsAndActionType(filterName, distributionSet, ActionType.DOWNLOAD_ONLY); + } + @Step private void verifyAutoAssignmentWithInvalidActionType(final TargetFilterQuery targetFilterQuery, final DistributionSet distributionSet) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerTest.java index bfa9885ec..ae230a24f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignCheckerTest.java @@ -211,36 +211,46 @@ public class AutoAssignCheckerTest extends AbstractJpaIntegrationTest { } @Test - @Description("Test auto assignment of a distribution set with FORCED and SOFT action types") + @Description("Test auto assignment of a distribution set with FORCED, SOFT and DOWNLOAD_ONLY action types") public void checkAutoAssignWithDifferentActionTypes() { final DistributionSet distributionSet = testdataFactory.createDistributionSet(); - final String targetDsAIdPref = "targA"; - final String targetDsBIdPref = "targB"; + final String targetDsAIdPref = "A"; + final String targetDsBIdPref = "B"; + final String targetDsCIdPref = "C"; - final List targetsA = testdataFactory.createTargets(5, targetDsAIdPref, - targetDsAIdPref.concat(" description")); - final List targetsB = testdataFactory.createTargets(10, targetDsBIdPref, - targetDsBIdPref.concat(" description")); - final int targetsCount = targetsA.size() + targetsB.size(); + List targetsA = createTargetsAndAutoAssignDistSet(targetDsAIdPref, 5, distributionSet, + ActionType.FORCED); + List targetsB = createTargetsAndAutoAssignDistSet(targetDsBIdPref, 10, distributionSet, + ActionType.SOFT); + List targetsC = createTargetsAndAutoAssignDistSet(targetDsCIdPref, 10, distributionSet, + ActionType.DOWNLOAD_ONLY); - targetFilterQueryManagement - .updateAutoAssignDSWithActionType( - targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("filterA") - .query("id==" + targetDsAIdPref + "*")).getId(), - distributionSet.getId(), ActionType.FORCED); - targetFilterQueryManagement - .updateAutoAssignDSWithActionType( - targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("filterB") - .query("id==" + targetDsBIdPref + "*")).getId(), - distributionSet.getId(), ActionType.SOFT); + final int targetsCount = targetsA.size() + targetsB.size() + targetsC.size(); autoAssignChecker.check(); verifyThatTargetsHaveDistributionSetAssignment(distributionSet, targetsA, targetsCount); verifyThatTargetsHaveDistributionSetAssignment(distributionSet, targetsB, targetsCount); + verifyThatTargetsHaveDistributionSetAssignment(distributionSet, targetsC, targetsCount); verifyThatTargetsHaveAssignmentActionType(ActionType.FORCED, targetsA); verifyThatTargetsHaveAssignmentActionType(ActionType.SOFT, targetsB); + verifyThatTargetsHaveAssignmentActionType(ActionType.DOWNLOAD_ONLY, targetsC); + } + + @Step + private List createTargetsAndAutoAssignDistSet(final String prefix, final int targetCount, + final DistributionSet distributionSet, final ActionType actionType) { + + final List targets = testdataFactory.createTargets(targetCount, "target" + prefix, + prefix.concat(" description")); + + targetFilterQueryManagement + .updateAutoAssignDSWithActionType( + targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create() + .name("filter" + prefix).query("id==target" + prefix + "*")).getId(), + distributionSet.getId(), actionType); + return targets; } @Step diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java index 7c707099b..ed4146458 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java @@ -225,8 +225,14 @@ public abstract class AbstractIntegrationTest { }; protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final String controllerId) { - return deploymentManagement.assignDistributionSet(dsID, Arrays.asList( - new TargetWithActionType(controllerId, ActionType.FORCED, RepositoryModelConstants.NO_FORCE_TIME))); + return assignDistributionSet(dsID, controllerId, ActionType.FORCED); + } + + protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final String controllerId, + final ActionType actionType) { + return deploymentManagement.assignDistributionSet(dsID, + Collections.singletonList(new TargetWithActionType(controllerId, actionType, + RepositoryModelConstants.NO_FORCE_TIME))); } /** @@ -316,8 +322,8 @@ public abstract class AbstractIntegrationTest { final boolean isRequiredMigrationStep) { final DistributionSet ds = testdataFactory.createDistributionSet(distributionSet, isRequiredMigrationStep); Target savedTarget = testdataFactory.createTarget(controllerId); - savedTarget = assignDistributionSet(ds.getId(), savedTarget.getControllerId()).getAssignedEntity().iterator() - .next(); + savedTarget = assignDistributionSet(ds.getId(), savedTarget.getControllerId(), ActionType.FORCED) + .getAssignedEntity().iterator().next(); Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()) .getContent().get(0); diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java index 5dcf1f9ee..7f38983dc 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java @@ -1003,14 +1003,41 @@ public class TestdataFactory { public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription, final int groupSize, final String filterQuery, final DistributionSet distributionSet, final String successCondition, final String errorCondition) { + return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet, successCondition, errorCondition, Action.ActionType.FORCED); + } + + /** + * Creates rollout based on given parameters. + * + * @param rolloutName + * of the {@link Rollout} + * @param rolloutDescription + * of the {@link Rollout} + * @param groupSize + * of the {@link Rollout} + * @param filterQuery + * to identify the {@link Target}s + * @param distributionSet + * to assign + * @param successCondition + * to switch to next group + * @param errorCondition + * to switch to next group + * @param actionType + * the type of the Rollout + * @return created {@link Rollout} + */ + public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription, + final int groupSize, final String filterQuery, final DistributionSet distributionSet, + final String successCondition, final String errorCondition, final Action.ActionType actionType) { final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults() .successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition) .errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition) .errorAction(RolloutGroupErrorAction.PAUSE, null).build(); final Rollout rollout = rolloutManagement.create(entityFactory.rollout().create().name(rolloutName) - .description(rolloutDescription).targetFilterQuery(filterQuery).set(distributionSet), groupSize, - conditions); + .description(rolloutDescription).targetFilterQuery(filterQuery).set(distributionSet) + .actionType(actionType), groupSize, conditions); // Run here, because Scheduler is disabled during tests rolloutManagement.handleRollouts(); diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java index fc6158593..df9977ffc 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java @@ -57,8 +57,8 @@ public class MgmtAction extends MgmtBaseEntity { @JsonProperty private Long forceTime; - @JsonProperty - private MgmtActionType forceType; + @JsonProperty(value="forceType") + private MgmtActionType actionType; @JsonProperty private MgmtMaintenanceWindow maintenanceWindow; @@ -79,12 +79,12 @@ public class MgmtAction extends MgmtBaseEntity { this.forceTime = forceTime; } - public MgmtActionType getForceType() { - return forceType; + public MgmtActionType getActionType() { + return actionType; } - public void setForceType(final MgmtActionType forceType) { - this.forceType = forceType; + public void setActionType(final MgmtActionType actionType) { + this.actionType = actionType; } public String getStatus() { diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionRequestBodyPut.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionRequestBodyPut.java index 0497481cf..d7b7ee8fd 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionRequestBodyPut.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionRequestBodyPut.java @@ -18,15 +18,15 @@ import com.fasterxml.jackson.annotation.JsonProperty; */ public class MgmtActionRequestBodyPut { - @JsonProperty - private MgmtActionType forceType; + @JsonProperty(value="forceType") + private MgmtActionType actionType; - public MgmtActionType getForceType() { - return forceType; + public MgmtActionType getActionType() { + return actionType; } - public void setForceType(final MgmtActionType forceType) { - this.forceType = forceType; + public void setActionType(final MgmtActionType actionType) { + this.actionType = actionType; } } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtActionType.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtActionType.java index d95375f5a..0601b954a 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtActionType.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtActionType.java @@ -28,7 +28,12 @@ public enum MgmtActionType { /** * The time forced action type. */ - TIMEFORCED("timeforced"); + TIMEFORCED("timeforced"), + + /** + * The Download-Only action type. + */ + DOWNLOAD_ONLY("downloadonly"); private final String name; diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java index 3a32e060a..454bfc994 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutResponseBody.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; /** * @@ -43,6 +44,9 @@ public class MgmtRolloutResponseBody extends MgmtNamedEntity { @JsonProperty private boolean deleted; + @JsonProperty + private MgmtActionType type; + public boolean isDeleted() { return deleted; } @@ -102,4 +106,12 @@ public class MgmtRolloutResponseBody extends MgmtNamedEntity { totalTargetsPerStatus.put(status, totalTargetCountByStatus); } + + public void setType(final MgmtActionType type) { + this.type = type; + } + + public MgmtActionType getType() { + return type; + } } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/tag/MgmtAssignedDistributionSetRequestBody.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/tag/MgmtAssignedDistributionSetRequestBody.java index 469780abf..4c004b206 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/tag/MgmtAssignedDistributionSetRequestBody.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/tag/MgmtAssignedDistributionSetRequestBody.java @@ -28,9 +28,8 @@ public class MgmtAssignedDistributionSetRequestBody { return distributionSetId; } - public MgmtAssignedDistributionSetRequestBody setDistributionSetId(final Long distributionSetId) { + public void setDistributionSetId(final Long distributionSetId) { this.distributionSetId = distributionSetId; - return this; } } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java index 95137b1ba..dc5a46411 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java @@ -20,7 +20,7 @@ import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity; * back. * */ -final class MgmtRestModelMapper { +public final class MgmtRestModelMapper { // private constructor, utility class private MgmtRestModelMapper() { @@ -66,6 +66,8 @@ final class MgmtRestModelMapper { return ActionType.FORCED; case TIMEFORCED: return ActionType.TIMEFORCED; + case DOWNLOAD_ONLY: + return ActionType.DOWNLOAD_ONLY; default: throw new IllegalStateException("Action Type is not supported"); } @@ -92,6 +94,8 @@ final class MgmtRestModelMapper { return MgmtActionType.FORCED; case TIMEFORCED: return MgmtActionType.TIMEFORCED; + case DOWNLOAD_ONLY: + return MgmtActionType.DOWNLOAD_ONLY; default: throw new IllegalStateException("Action Type is not supported"); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java index 87638e58f..f380c106f 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java @@ -78,6 +78,7 @@ final class MgmtRolloutMapper { body.setStatus(rollout.getStatus().toString().toLowerCase()); body.setTotalTargets(rollout.getTotalTargets()); body.setDeleted(rollout.isDeleted()); + body.setType(MgmtRestModelMapper.convertActionType(rollout.getActionType())); if (withDetails) { for (final TotalTargetCountStatus.Status status : TotalTargetCountStatus.Status.values()) { diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java index 782084361..b34f62967 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java @@ -205,7 +205,7 @@ public final class MgmtTargetMapper { if (ActionType.TIMEFORCED.equals(action.getActionType())) { result.setForceTime(action.getForcedTime()); } - result.setForceType(MgmtRestModelMapper.convertActionType(action.getActionType())); + result.setActionType(MgmtRestModelMapper.convertActionType(action.getActionType())); if (action.isActive()) { result.setStatus(MgmtAction.ACTION_PENDING); diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java index 9e64d84e1..7e6790b60 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java @@ -346,7 +346,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi { return ResponseEntity.notFound().build(); } - if (!MgmtActionType.FORCED.equals(actionUpdate.getForceType())) { + if (!MgmtActionType.FORCED.equals(actionUpdate.getActionType())) { throw new ValidationException("Resource supports only switch to FORCED."); } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java index d06121c1a..740d461bd 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java @@ -33,6 +33,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; @@ -1245,4 +1246,30 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr return created; } + @Test + @Description("Ensures that multi target assignment through API is reflected by the repository in the case of " + + "DOWNLOAD_ONLY.") + public void assignMultipleTargetsToDistributionSetAsDownloadOnly() throws Exception { + final DistributionSet createdDs = testdataFactory.createDistributionSet(); + + // prepare targets + final String[] knownTargetIds = new String[] { "1", "2", "3", "4", "5" }; + final JSONArray list = new JSONArray(); + for (final String targetId : knownTargetIds) { + testdataFactory.createTarget(targetId); + list.put(new JSONObject().put("id", Long.valueOf(targetId))); + } + // assign already one target to DS + assignDistributionSet(createdDs.getId(), knownTargetIds[0], Action.ActionType.DOWNLOAD_ONLY); + + mvc.perform(post("/rest/v1/distributionsets/{ds}/assignedTargets", createdDs.getId()) + .contentType(MediaType.APPLICATION_JSON).content(list.toString())) + .andExpect(status().isOk()).andExpect(jsonPath("$.assigned", equalTo(knownTargetIds.length - 1))) + .andExpect(jsonPath("$.alreadyAssigned", equalTo(1))) + .andExpect(jsonPath("$.total", equalTo(knownTargetIds.length))); + + assertThat(targetManagement.findByAssignedDistributionSet(PAGE, createdDs.getId()).getContent()) + .as("Five targets in repository have DS assigned").hasSize(5); + } + } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java index 13a84c44a..7835531e8 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java @@ -32,6 +32,7 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; @@ -130,7 +131,7 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes testdataFactory.createTargets(20, "target", "rollout"); final DistributionSet dsA = testdataFactory.createDistributionSet(""); - postRollout("rollout1", 10, dsA.getId(), "id==target*", 20); + postRollout("rollout1", 10, dsA.getId(), "id==target*", 20, Action.ActionType.FORCED); } @Test @@ -367,8 +368,8 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes testdataFactory.createTargets(20, "target", "rollout"); // setup - create 2 rollouts - postRollout("rollout1", 10, dsA.getId(), "id==target*", 20); - postRollout("rollout2", 5, dsA.getId(), "id==target-0001*", 10); + postRollout("rollout1", 10, dsA.getId(), "id==target*", 20, Action.ActionType.FORCED); + postRollout("rollout2", 5, dsA.getId(), "id==target-0001*", 10, Action.ActionType.FORCED); // Run here, because Scheduler is disabled during tests rolloutManagement.handleRollouts(); @@ -408,8 +409,8 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes testdataFactory.createTargets(20, "target", "rollout"); // setup - create 2 rollouts - postRollout("rollout1", 10, dsA.getId(), "id==target*", 20); - postRollout("rollout2", 5, dsA.getId(), "id==target*", 20); + postRollout("rollout1", 10, dsA.getId(), "id==target*", 20, Action.ActionType.FORCED); + postRollout("rollout2", 5, dsA.getId(), "id==target*", 20, Action.ActionType.FORCED); // Run here, because Scheduler is disabled during tests rolloutManagement.handleRollouts(); @@ -906,6 +907,15 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes } + @Test + @Description("Verifies that a DOWNLOAD_ONLY rollout is possible") + public void createDownloadOnlyRollout() throws Exception { + testdataFactory.createTargets(20, "target", "rollout"); + + final DistributionSet dsA = testdataFactory.createDistributionSet(""); + postRollout("rollout1", 10, dsA.getId(), "id==target*", 20, Action.ActionType.DOWNLOAD_ONLY); + } + protected T doWithTimeout(final Callable callable, final SuccessCondition successCondition, final long timeout, final long pollInterval) throws Exception // NOPMD { @@ -944,13 +954,17 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes } private void postRollout(final String name, final int groupSize, final Long distributionSetId, - final String targetFilterQuery, final int targets) throws Exception { + final String targetFilterQuery, final int targets, final Action.ActionType type) throws Exception { + String actionType = MgmtRestModelMapper.convertActionType(type).getName(); + String rollout = JsonBuilder.rollout(name, "desc", groupSize, distributionSetId, targetFilterQuery, + new RolloutGroupConditionBuilder().withDefaults().build(), null, actionType); + mvc.perform(post("/rest/v1/rollouts") - .content(JsonBuilder.rollout(name, "desc", groupSize, distributionSetId, targetFilterQuery, - new RolloutGroupConditionBuilder().withDefaults().build())) + .content(rollout) .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) .andExpect(jsonPath("$.name", equalTo(name))).andExpect(jsonPath("$.status", equalTo("creating"))) + .andExpect(jsonPath("$.type", equalTo(actionType))) .andExpect(jsonPath("$.targetFilterQuery", equalTo(targetFilterQuery))) .andExpect(jsonPath("$.description", equalTo("desc"))) .andExpect(jsonPath("$.distributionSetId", equalTo(distributionSetId.intValue()))) diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java index da2523ec9..fdeadcbd1 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.mgmt.rest.resource; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.rest.util.MockMvcResultPrinter.print; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasSize; @@ -104,7 +105,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte public void updateTargetWhichDoesNotExistsLeadsToEntityNotFound() throws Exception { final String notExistingId = "4395"; mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + notExistingId).content("{}") - .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .contentType(MediaType.APPLICATION_JSON)).andDo(print()) .andExpect(status().isNotFound()); } @@ -120,7 +121,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte final TargetFilterQuery tfq = createSingleTargetFilterQuery(filterName, filterQuery); mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId()).content(body) - .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .contentType(MediaType.APPLICATION_JSON)).andDo(print()).andExpect(status().isOk()) .andExpect(jsonPath(JSON_PATH_ID, equalTo(tfq.getId().intValue()))) .andExpect(jsonPath(JSON_PATH_QUERY, equalTo(filterQuery2))) .andExpect(jsonPath(JSON_PATH_NAME, equalTo(filterName))); @@ -143,7 +144,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte .create(entityFactory.targetFilterQuery().create().name(filterName).query(filterQuery)); mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId()).content(body) - .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .contentType(MediaType.APPLICATION_JSON)).andDo(print()).andExpect(status().isOk()) .andExpect(jsonPath(JSON_PATH_ID, equalTo(tfq.getId().intValue()))) .andExpect(jsonPath(JSON_PATH_QUERY, equalTo(filterQuery))) .andExpect(jsonPath(JSON_PATH_NAME, equalTo(filterName2))); @@ -167,7 +168,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte createSingleTargetFilterQuery(idC, testQuery); mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING)).andExpect(status().isOk()) - .andDo(MockMvcResultPrinter.print()) + .andDo(print()) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(knownTargetAmount))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(knownTargetAmount))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(knownTargetAmount))) @@ -198,7 +199,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING) .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(limitSize))) - .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andDo(print()) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(knownTargetAmount))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(limitSize))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(limitSize))) @@ -227,7 +228,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING) .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_OFFSET, String.valueOf(offsetParam)) .param(MgmtRestConstants.REQUEST_PARAMETER_PAGING_LIMIT, String.valueOf(knownTargetAmount))) - .andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andDo(print()) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(knownTargetAmount))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(expectedSize))) .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(expectedSize))) @@ -253,7 +254,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte + tfq.getId(); // test mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId())) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(print()).andExpect(status().isOk()) .andExpect(jsonPath(JSON_PATH_NAME, equalTo(knownName))) .andExpect(jsonPath(JSON_PATH_QUERY, equalTo(knownQuery))) .andExpect(jsonPath("$._links.self.href", equalTo(hrefPrefix))) @@ -284,7 +285,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte final MvcResult mvcResult = mvc .perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING).content(notJson) .contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()).andReturn(); + .andDo(print()).andExpect(status().isBadRequest()).andReturn(); assertThat(targetFilterQueryManagement.count()).isEqualTo(0); @@ -311,7 +312,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform( post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId() + "/autoAssignDS") .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()) + .andDo(print()).andExpect(status().isForbidden()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(QuotaExceededException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); } @@ -333,7 +334,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform( post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId() + "/autoAssignDS") .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + .andDo(print()).andExpect(status().isOk()); final TargetFilterQuery updatedFilterQuery = targetFilterQueryManagement.get(filterQuery.getId()).get(); @@ -343,7 +344,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte // update the query of the filter query to trigger a quota hit mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId()) .content("{\"query\":\"controllerId==target*\"}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()) + .andDo(print()).andExpect(status().isForbidden()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(QuotaExceededException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); @@ -366,6 +367,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte verifyAutoAssignmentWithTimeForcedActionType(tfq, set); + verifyAutoAssignmentWithDownloadOnlyActionType(tfq, set); + verifyAutoAssignmentWithUnknownActionType(tfq, set); verifyAutoAssignmentWithIncompleteDs(tfq); @@ -400,7 +403,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte ? "{\"id\":" + set.getId() + ", \"type\":\"" + actionType.getName() + "\"}" : "{\"id\":" + set.getId() + "}"; mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") - .content(payload).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .content(payload).contentType(MediaType.APPLICATION_JSON)).andDo(print()) .andExpect(status().isOk()); final TargetFilterQuery updatedFilterQuery = targetFilterQueryManagement.get(tfq.getId()).get(); @@ -411,7 +414,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte .isEqualTo(MgmtRestModelMapper.convertActionType(expectedActionType)); mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId())) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andDo(print()).andExpect(status().isOk()) .andExpect(jsonPath(JSON_PATH_NAME, equalTo(tfq.getName()))) .andExpect(jsonPath(JSON_PATH_QUERY, equalTo(tfq.getQuery()))) .andExpect(jsonPath(JSON_PATH_AUTO_ASSIGN_DS, equalTo(set.getId().intValue()))) @@ -425,7 +428,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte throws Exception { mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") .content("{\"id\":" + set.getId() + ", \"type\":\"" + MgmtActionType.TIMEFORCED.getName() + "\"}") - .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .contentType(MediaType.APPLICATION_JSON)).andDo(print()) .andExpect(status().isBadRequest()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(InvalidAutoAssignActionTypeException.class.getName()))) @@ -433,12 +436,18 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte equalTo(SpServerError.SP_AUTO_ASSIGN_ACTION_TYPE_INVALID.getKey()))); } + @Step + private void verifyAutoAssignmentWithDownloadOnlyActionType(final TargetFilterQuery tfq, final DistributionSet set) + throws Exception { + verifyAutoAssignmentByActionType(tfq, set, MgmtActionType.DOWNLOAD_ONLY); + } + @Step private void verifyAutoAssignmentWithUnknownActionType(final TargetFilterQuery tfq, final DistributionSet set) throws Exception { mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") .content("{\"id\":" + set.getId() + ", \"type\":\"unknown\"}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()) + .andDo(print()).andExpect(status().isBadRequest()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(MessageNotReadableException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_REST_BODY_NOT_READABLE.getKey()))); } @@ -451,7 +460,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") .content("{\"id\":" + incompleteDistributionSet.getId() + "}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()) + .andDo(print()).andExpect(status().isBadRequest()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(InvalidAutoAssignDistributionSetException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, @@ -466,7 +475,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") .content("{\"id\":" + softDeletedDs.getId() + "}").contentType(MediaType.APPLICATION_JSON)) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()) + .andDo(print()).andExpect(status().isBadRequest()) .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(InvalidAutoAssignDistributionSetException.class.getName()))) .andExpect(jsonPath(JSON_PATH_ERROR_CODE, diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java index 9e9159cbb..d8d2ce187 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java @@ -1287,6 +1287,26 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest assertThat(targetManagement.getByControllerID(target.getControllerId()).get()).isEqualTo(target); } + @Test + @Description("Verfies that a DOWNLOAD_ONLY DS to target assignment is properly handled") + public void assignDownloadOnlyDistributionSetToTarget() throws Exception { + + Target target = testdataFactory.createTarget(); + final DistributionSet set = testdataFactory.createDistributionSet("one"); + + mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/assignedDS") + .content("{\"id\":" + set.getId() + ",\"type\": \"downloadonly\"}") + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("assigned", equalTo(1))).andExpect(jsonPath("alreadyAssigned", equalTo(0))) + .andExpect(jsonPath("total", equalTo(1))); + + assertThat(deploymentManagement.getAssignedDistributionSet(target.getControllerId()).get()).isEqualTo(set); + Slice actions = deploymentManagement.findActionsByTarget("targetExist", PageRequest.of(0, 100)); + assertThat(actions.getSize()).isGreaterThan(0); + actions.stream().filter(a -> a.getDistributionSet().equals(set)) + .forEach(a -> ActionType.DOWNLOAD_ONLY.equals(a.getActionType())); + } + @Test @Description("Verfies that an offline DS to target assignment is reflected by the repository and that repeating " + "the assignment does not change the target.") diff --git a/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java b/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java index 7428d7b91..507f26ab3 100644 --- a/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java +++ b/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java @@ -449,12 +449,18 @@ public abstract class JsonBuilder { public static String rollout(final String name, final String description, final int groupSize, final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions) { - return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null); + return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null, null); } public static String rollout(final String name, final String description, final Integer groupSize, - final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions, - final List groups) { + final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions, + final List groups) { + return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, groups, null); + } + + public static String rollout(final String name, final String description, final Integer groupSize, + final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions, + final List groups, final String type) { final JSONObject json = new JSONObject(); try { json.put("name", name); @@ -463,6 +469,10 @@ public abstract class JsonBuilder { json.put("distributionSetId", distributionSetId); json.put("targetFilterQuery", targetFilterQuery); + if(type != null){ + json.put("type", type); + } + if (conditions != null) { final JSONObject successCondition = new JSONObject(); diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java index 43ee51821..2e0c0998c 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java @@ -84,6 +84,7 @@ public final class MgmtApiModelProperties { public static final String ROLLOUT_TOTAL_TARGETS = "the total targets of a rollout"; public static final String ROLLOUT_TOTAL_TARGETS_PER_STATUS = "the total targets per status"; public static final String ROLLOUT_STATUS = "the status of this rollout"; + public static final String ROLLOUT_TYPE = "the type of this rollout"; public static final String ROLLOUT_GROUP_STATUS = "the status of this rollout group"; public static final String ROLLOUT_AMOUNT_GROUPS = "the amount of groups the rollout should split targets into"; public static final String ROLLOUT_GROUPS = "the list of group definitions"; diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java index eb680bb3f..1d8f4d7ee 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/DistributionSetsDocumentationTest.java @@ -391,7 +391,7 @@ public class DistributionSetsDocumentationTest extends AbstractApiRestDocumentat requestFieldWithPath("[]maintenanceWindow.timezone") .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE).optional(), requestFieldWithPath("[]type").description(MgmtApiModelProperties.FORCETIME_TYPE) - .attributes(key("value").value("['soft', 'forced','timeforced']"))), + .attributes(key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))), responseFields( fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS), fieldWithPath("alreadyAssigned").type(JsonFieldType.NUMBER) diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java index 5c6923268..6e1c63526 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/RolloutResourceDocumentationTest.java @@ -130,6 +130,8 @@ public class RolloutResourceDocumentationTest extends AbstractApiRestDocumentati fieldWithPath(arrayPrefix + "distributionSetId").description(MgmtApiModelProperties.ROLLOUT_DS_ID)); allFieldDescriptor.add(fieldWithPath(arrayPrefix + "status").description(MgmtApiModelProperties.ROLLOUT_STATUS) .attributes(key("value").value("['creating','ready','paused','running','finished']"))); + allFieldDescriptor.add(fieldWithPath(arrayPrefix + "type").description(MgmtApiModelProperties.ROLLOUT_TYPE) + .attributes(key("value").value("['forced','soft','timeforced','downloadonly']"))); allFieldDescriptor.add( fieldWithPath(arrayPrefix + "totalTargets").description(MgmtApiModelProperties.ROLLOUT_TOTAL_TARGETS)); allFieldDescriptor.add(fieldWithPath(arrayPrefix + "_links.self").ignored()); diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java index 831122a97..85b846fe6 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetFilterQueriesResourceDocumentationTest.java @@ -81,7 +81,7 @@ public class TargetFilterQueriesResourceDocumentationTest extends AbstractApiRes fieldWithPath("content[].autoAssignActionType") .description(MgmtApiModelProperties.ACTION_FORCE_TYPE) .type(JsonFieldType.STRING.toString()) - .attributes(key("value").value("['forced', 'soft']")), + .attributes(key("value").value("['forced', 'soft', 'downloadonly']")), fieldWithPath("content[].createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), fieldWithPath("content[].createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), fieldWithPath("content[].lastModifiedAt") @@ -199,7 +199,7 @@ public class TargetFilterQueriesResourceDocumentationTest extends AbstractApiRes requestFields(requestFieldWithPath("id").description(MgmtApiModelProperties.DS_ID), optionalRequestFieldWithPath("type") .description(MgmtApiModelProperties.ACTION_FORCE_TYPE) - .attributes(key("value").value("['forced', 'soft']"))), + .attributes(key("value").value("['forced', 'soft', 'downloadonly']"))), getResponseFieldTargetFilterQuery(false))); } @@ -226,7 +226,7 @@ public class TargetFilterQueriesResourceDocumentationTest extends AbstractApiRes .type(JsonFieldType.NUMBER.toString()), fieldWithPath(arrayPrefix + "autoAssignActionType") .description(MgmtApiModelProperties.ACTION_FORCE_TYPE).type(JsonFieldType.STRING.toString()) - .attributes(key("value").value("['forced', 'soft']")), + .attributes(key("value").value("['forced', 'soft', 'downloadonly']")), fieldWithPath(arrayPrefix + "createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), fieldWithPath(arrayPrefix + "createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), fieldWithPath(arrayPrefix + "lastModifiedAt").description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT), diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java index 248b4093d..64236952b 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TargetResourceDocumentationTest.java @@ -519,7 +519,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio requestFieldWithPath("maintenanceWindow.timezone") .description(MgmtApiModelProperties.MAINTENANCE_WINDOW_TIMEZONE).optional(), requestFieldWithPath("type").description(MgmtApiModelProperties.FORCETIME_TYPE) - .attributes(key("value").value("['soft', 'forced','timeforced']"))), + .attributes(key("value").value("['soft', 'forced','timeforced', 'downloadonly']"))), responseFields( fieldWithPath("assigned").description(MgmtApiModelProperties.DS_NEW_ASSIGNED_TARGETS), fieldWithPath("alreadyAssigned").type(JsonFieldType.NUMBER) diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java index 18a1de6cc..35df61e2a 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java @@ -134,7 +134,7 @@ public abstract class AbstractGrid extends Grid implements Re setColumnProperties(); setColumnHeaderNames(); setColumnsHidable(); - addColumnRenderes(); + addColumnRenderers(); setColumnExpandRatio(); setHiddenColumns(); @@ -327,7 +327,7 @@ public abstract class AbstractGrid extends Grid implements Re * Template method invoked by {@link #addNewContainerDS()} for adding * special column renderers if needed. */ - protected abstract void addColumnRenderes(); + protected abstract void addColumnRenderers(); /** * Template method invoked by {@link #addNewContainerDS()} that hides diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/TargetAssignmentOperations.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/TargetAssignmentOperations.java new file mode 100644 index 000000000..b77f739ce --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/TargetAssignmentOperations.java @@ -0,0 +1,286 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.management; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; +import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; +import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; +import org.eclipse.hawkbit.repository.model.TargetWithActionType; +import org.eclipse.hawkbit.ui.UiProperties; +import org.eclipse.hawkbit.ui.common.confirmwindow.layout.ConfirmationTab; +import org.eclipse.hawkbit.ui.common.entity.DistributionSetIdName; +import org.eclipse.hawkbit.ui.common.entity.TargetIdName; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.management.event.PinUnpinEvent; +import org.eclipse.hawkbit.ui.management.event.SaveActionWindowEvent; +import org.eclipse.hawkbit.ui.management.miscs.AbstractActionTypeOptionGroupLayout; +import org.eclipse.hawkbit.ui.management.miscs.ActionTypeOptionGroupAssignmentLayout; +import org.eclipse.hawkbit.ui.management.miscs.MaintenanceWindowLayout; +import org.eclipse.hawkbit.ui.management.state.ManagementUIState; +import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; +import org.eclipse.hawkbit.ui.utils.UINotification; +import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vaadin.spring.events.EventBus.UIEventBus; + +import com.google.common.collect.Maps; +import com.vaadin.data.Property; +import com.vaadin.ui.CheckBox; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Link; +import com.vaadin.ui.themes.ValoTheme; + +/** + * Helper Class for Target Assignment Operations from the Deployment View + */ +public final class TargetAssignmentOperations { + + private static final Logger LOG = LoggerFactory.getLogger(TargetAssignmentOperations.class); + + private TargetAssignmentOperations() { + } + + /** + * Save all target(s)-distributionSet assignments + * + * @param managementUIState + * the management UI state + * @param actionTypeOptionGroupLayout + * the action Type Option Group Layout + * @param maintenanceWindowLayout + * the Maintenance Window Layout + * @param deploymentManagement + * the Deployment Management + * @param notification + * the UI Notification + * @param eventBus + * the UI Event Bus + * @param i18n + * the Vaadin Message Source for multi language + * @param eventSource + * the source object for sending potential events + */ + public static void saveAllAssignments(final ManagementUIState managementUIState, + final ActionTypeOptionGroupAssignmentLayout actionTypeOptionGroupLayout, + final MaintenanceWindowLayout maintenanceWindowLayout, final DeploymentManagement deploymentManagement, + final UINotification notification, final UIEventBus eventBus, final VaadinMessageSource i18n, + final Object eventSource) { + final Set itemIds = managementUIState.getAssignedList().keySet(); + Long distId; + List targetIdSetList; + List tempIdList; + final ActionType actionType = ((AbstractActionTypeOptionGroupLayout.ActionTypeOption) actionTypeOptionGroupLayout + .getActionTypeOptionGroup().getValue()).getActionType(); + final long forcedTimeStamp = actionTypeOptionGroupLayout.getActionTypeOptionGroup() + .getValue() == AbstractActionTypeOptionGroupLayout.ActionTypeOption.AUTO_FORCED + ? actionTypeOptionGroupLayout.getForcedTimeDateField().getValue().getTime() + : RepositoryModelConstants.NO_FORCE_TIME; + + final Map> saveAssignedList = Maps.newHashMapWithExpectedSize(itemIds.size()); + + for (final TargetIdName itemId : itemIds) { + final DistributionSetIdName distitem = managementUIState.getAssignedList().get(itemId); + distId = distitem.getId(); + + if (saveAssignedList.containsKey(distId)) { + targetIdSetList = saveAssignedList.get(distId); + } else { + targetIdSetList = new ArrayList<>(); + } + targetIdSetList.add(itemId); + saveAssignedList.put(distId, targetIdSetList); + } + + final String maintenanceSchedule = maintenanceWindowLayout.getMaintenanceSchedule(); + final String maintenanceDuration = maintenanceWindowLayout.getMaintenanceDuration(); + final String maintenanceTimeZone = maintenanceWindowLayout.getMaintenanceTimeZone(); + + for (final Map.Entry> mapEntry : saveAssignedList.entrySet()) { + tempIdList = saveAssignedList.get(mapEntry.getKey()); + final DistributionSetAssignmentResult distributionSetAssignmentResult = deploymentManagement + .assignDistributionSet(mapEntry.getKey(), + tempIdList.stream().map(t -> maintenanceWindowLayout.isEnabled() + ? new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp, + maintenanceSchedule, maintenanceDuration, maintenanceTimeZone) + : new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp)) + .collect(Collectors.toList())); + if (distributionSetAssignmentResult.getAssigned() > 0) { + notification.displaySuccess( + i18n.getMessage("message.target.assignment", distributionSetAssignmentResult.getAssigned())); + } + if (distributionSetAssignmentResult.getAlreadyAssigned() > 0) { + notification.displaySuccess(i18n.getMessage("message.target.alreadyAssigned", + distributionSetAssignmentResult.getAlreadyAssigned())); + } + } + refreshPinnedDetails(saveAssignedList, managementUIState, eventBus, eventSource); + managementUIState.getAssignedList().clear(); + notification.displaySuccess(i18n.getMessage("message.target.ds.assign.success")); + eventBus.publish(eventSource, SaveActionWindowEvent.SAVED_ASSIGNMENTS); + } + + private static void refreshPinnedDetails(final Map> saveAssignedList, + final ManagementUIState managementUIState, final UIEventBus eventBus, final Object eventSource) { + final Optional pinnedDist = managementUIState.getTargetTableFilters().getPinnedDistId(); + final Optional pinnedTarget = managementUIState.getDistributionTableFilters().getPinnedTarget(); + + if (pinnedDist.isPresent()) { + if (saveAssignedList.keySet().contains(pinnedDist.get())) { + eventBus.publish(eventSource, PinUnpinEvent.PIN_DISTRIBUTION); + } + } else if (pinnedTarget.isPresent()) { + final Set assignedTargetIds = managementUIState.getAssignedList().keySet(); + if (assignedTargetIds.contains(pinnedTarget.get())) { + eventBus.publish(eventSource, PinUnpinEvent.PIN_TARGET); + } + } + } + + /** + * Check wether the maintenance window is valid or not + * + * @param maintenanceWindowLayout + * the maintenance window layout + * @param notification + * the UI Notification + * @return boolean if maintenance window is valid or not + */ + public static boolean isMaintenanceWindowValid(final MaintenanceWindowLayout maintenanceWindowLayout, + final UINotification notification) { + if (maintenanceWindowLayout.isEnabled()) { + try { + MaintenanceScheduleHelper.validateMaintenanceSchedule(maintenanceWindowLayout.getMaintenanceSchedule(), + maintenanceWindowLayout.getMaintenanceDuration(), + maintenanceWindowLayout.getMaintenanceTimeZone()); + } catch (final InvalidMaintenanceScheduleException e) { + LOG.error("Maintenance window is not valid", e); + notification.displayValidationError(e.getMessage()); + return false; + } + } + return true; + } + + /** + * Create the Assignment Confirmation Tab + * + * @param actionTypeOptionGroupLayout + * the action Type Option Group Layout + * @param maintenanceWindowLayout + * the Maintenance Window Layout + * @param saveButtonToggle + * The event listener to derimne if save button should be enabled or not + * @param i18n + * the Vaadin Message Source for multi language + * @param uiProperties + * the UI Properties + * @return the Assignment Confirmation tab + */ + public static ConfirmationTab createAssignmentTab( + final ActionTypeOptionGroupAssignmentLayout actionTypeOptionGroupLayout, + final MaintenanceWindowLayout maintenanceWindowLayout, final Consumer saveButtonToggle, + final VaadinMessageSource i18n, final UiProperties uiProperties) { + + final CheckBox maintenanceWindowControl = maintenanceWindowControl(i18n, maintenanceWindowLayout, + saveButtonToggle); + final Link maintenanceWindowHelpLink = maintenanceWindowHelpLinkControl(uiProperties, i18n); + final HorizontalLayout layout = createHorizontalLayout(maintenanceWindowControl, maintenanceWindowHelpLink); + actionTypeOptionGroupLayout.selectDefaultOption(); + + initMaintenanceWindow(maintenanceWindowLayout, saveButtonToggle); + addValueChangeListener(actionTypeOptionGroupLayout, maintenanceWindowControl, maintenanceWindowHelpLink); + return createAssignmentTab(actionTypeOptionGroupLayout, layout, maintenanceWindowLayout); + } + + private static HorizontalLayout createHorizontalLayout(final CheckBox maintenanceWindowControl, + final Link maintenanceWindowHelpLink) { + final HorizontalLayout layout = new HorizontalLayout(); + layout.addComponent(maintenanceWindowControl); + layout.addComponent(maintenanceWindowHelpLink); + return layout; + } + + private static ConfirmationTab createAssignmentTab( + final ActionTypeOptionGroupAssignmentLayout actionTypeOptionGroupLayout, final HorizontalLayout layout, + final MaintenanceWindowLayout maintenanceWindowLayout) { + final ConfirmationTab assignmentTab = new ConfirmationTab(); + assignmentTab.addComponent(actionTypeOptionGroupLayout); + assignmentTab.addComponent(layout); + assignmentTab.addComponent(maintenanceWindowLayout); + return assignmentTab; + } + + private static void initMaintenanceWindow(final MaintenanceWindowLayout maintenanceWindowLayout, + final Consumer saveButtonToggle) { + maintenanceWindowLayout.setVisible(false); + maintenanceWindowLayout.setEnabled(false); + maintenanceWindowLayout.getScheduleControl().addTextChangeListener( + event -> saveButtonToggle.accept(maintenanceWindowLayout.onScheduleChange(event))); + maintenanceWindowLayout.getDurationControl().addTextChangeListener( + event -> saveButtonToggle.accept(maintenanceWindowLayout.onDurationChange(event))); + } + + private static CheckBox maintenanceWindowControl(final VaadinMessageSource i18n, + final MaintenanceWindowLayout maintenanceWindowLayout, final Consumer saveButtonToggle) { + final CheckBox enableMaintenanceWindow = new CheckBox(i18n.getMessage("caption.maintenancewindow.enabled")); + enableMaintenanceWindow.setId(UIComponentIdProvider.MAINTENANCE_WINDOW_ENABLED_ID); + enableMaintenanceWindow.addStyleName(ValoTheme.CHECKBOX_SMALL); + enableMaintenanceWindow.addStyleName("dist-window-maintenance-window-enable"); + enableMaintenanceWindow.addValueChangeListener(event -> { + final Boolean isMaintenanceWindowEnabled = enableMaintenanceWindow.getValue(); + maintenanceWindowLayout.setVisible(isMaintenanceWindowEnabled); + maintenanceWindowLayout.setEnabled(isMaintenanceWindowEnabled); + saveButtonToggle.accept(!isMaintenanceWindowEnabled); + maintenanceWindowLayout.clearAllControls(); + }); + return enableMaintenanceWindow; + } + + private static void addValueChangeListener(final ActionTypeOptionGroupAssignmentLayout actionTypeOptionGroupLayout, + final CheckBox enableMaintenanceWindowControl, final Link maintenanceWindowHelpLinkControl) { + actionTypeOptionGroupLayout.getActionTypeOptionGroup() + .addValueChangeListener(new Property.ValueChangeListener() { + private static final long serialVersionUID = 1L; + + @Override + public void valueChange(final Property.ValueChangeEvent event) { + if (event.getProperty().getValue() + .equals(AbstractActionTypeOptionGroupLayout.ActionTypeOption.DOWNLOAD_ONLY)) { + enableMaintenanceWindowControl.setValue(false); + enableMaintenanceWindowControl.setEnabled(false); + maintenanceWindowHelpLinkControl.setEnabled(false); + + } else { + enableMaintenanceWindowControl.setEnabled(true); + maintenanceWindowHelpLinkControl.setEnabled(true); + } + + } + }); + } + + private static Link maintenanceWindowHelpLinkControl(final UiProperties uiProperties, + final VaadinMessageSource i18n) { + final String maintenanceWindowHelpUrl = uiProperties.getLinks().getDocumentation().getMaintenanceWindowView(); + return SPUIComponentProvider.getHelpLink(i18n, maintenanceWindowHelpUrl); + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java index f1cdc7fa9..6ab8d39ff 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java @@ -69,8 +69,10 @@ public class ActionHistoryGrid extends AbstractGrid { private static final String STATUS_ICON_NEUTRAL = "statusIconNeutral"; private static final String STATUS_ICON_ACTIVE = "statusIconActive"; private static final String STATUS_ICON_FORCED = "statusIconForced"; + private static final String STATUS_ICON_DOWNLOAD_ONLY = "statusIconDownloadOnly"; + private static final String STATUS_ICON_SOFT = "statusIconSoft"; - private static final String VIRT_PROP_FORCED = "forced"; + private static final String VIRT_PROP_TYPE = "type"; private static final String VIRT_PROP_TIMEFORCED = "timeForced"; private static final String VIRT_PROP_ACTION_CANCEL = "cancel-action"; private static final String VIRT_PROP_ACTION_FORCE = "force-action"; @@ -79,20 +81,20 @@ public class ActionHistoryGrid extends AbstractGrid { private static final Object[] maxColumnOrder = new Object[] { ProxyAction.PXY_ACTION_IS_ACTIVE_DECO, ProxyAction.PXY_ACTION_ID, ProxyAction.PXY_ACTION_DS_NAME_VERSION, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT, ProxyAction.PXY_ACTION_STATUS, ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW, - ProxyAction.PXY_ACTION_ROLLOUT_NAME, VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, + ProxyAction.PXY_ACTION_ROLLOUT_NAME, VIRT_PROP_TYPE, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT }; private static final Object[] minColumnOrder = new Object[] { ProxyAction.PXY_ACTION_IS_ACTIVE_DECO, ProxyAction.PXY_ACTION_DS_NAME_VERSION, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT, - ProxyAction.PXY_ACTION_STATUS, ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW, VIRT_PROP_FORCED, + ProxyAction.PXY_ACTION_STATUS, ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW, VIRT_PROP_TYPE, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT }; - private static final String[] leftAlignedColumns = new String[] { VIRT_PROP_TIMEFORCED }; + private static final String[] leftAlignedColumns = new String[] {}; private static final String[] centerAlignedColumns = new String[] { ProxyAction.PXY_ACTION_IS_ACTIVE_DECO, - ProxyAction.PXY_ACTION_STATUS }; + ProxyAction.PXY_ACTION_STATUS, VIRT_PROP_TYPE, ProxyAction.PXY_ACTION_ID, VIRT_PROP_TIMEFORCED }; - private static final String[] rightAlignedColumns = new String[] { VIRT_PROP_FORCED, ProxyAction.PXY_ACTION_ID }; + private static final String[] rightAlignedColumns = new String[] {}; private final transient DeploymentManagement deploymentManagement; private final UINotification notification; @@ -107,19 +109,7 @@ public class ActionHistoryGrid extends AbstractGrid { private final BeanQueryFactory targetQF = new BeanQueryFactory<>(ActionBeanQuery.class); - boolean forceClientRefreshToggle = true; - - /** - * Constructor. - * - * @param i18n - * @param deploymentManagement - * @param eventBus - * @param notification - * @param managementUIState - * @param permissionChecker - */ - protected ActionHistoryGrid(final VaadinMessageSource i18n, final DeploymentManagement deploymentManagement, + ActionHistoryGrid(final VaadinMessageSource i18n, final DeploymentManagement deploymentManagement, final UIEventBus eventBus, final UINotification notification, final ManagementUIState managementUIState, final SpPermissionChecker permissionChecker) { super(i18n, eventBus, permissionChecker); @@ -220,14 +210,14 @@ public class ActionHistoryGrid extends AbstractGrid { } @Override - protected void addColumnRenderes() { + protected void addColumnRenderers() { getColumn(ProxyAction.PXY_ACTION_LAST_MODIFIED_AT).setConverter(new LongToFormattedDateStringConverter()); getColumn(ProxyAction.PXY_ACTION_STATUS).setRenderer(new HtmlLabelRenderer(), new HtmlStatusLabelConverter(this::createStatusLabelMetadata)); getColumn(ProxyAction.PXY_ACTION_IS_ACTIVE_DECO).setRenderer(new HtmlLabelRenderer(), new HtmlIsActiveLabelConverter(this::createIsActiveLabelMetadata)); - getColumn(VIRT_PROP_FORCED).setRenderer(new HtmlLabelRenderer(), - new HtmlVirtPropLabelConverter(this::createForcedLabelMetadata)); + getColumn(VIRT_PROP_TYPE).setRenderer(new HtmlLabelRenderer(), + new HtmlVirtPropLabelConverter(this::createTypeLabelMetadata)); getColumn(VIRT_PROP_TIMEFORCED).setRenderer(new HtmlLabelRenderer(), new HtmlVirtPropLabelConverter(this::createTimeForcedLabelMetadata)); getColumn(VIRT_PROP_ACTION_CANCEL).setRenderer( @@ -270,14 +260,23 @@ public class ActionHistoryGrid extends AbstractGrid { return activeStates.get(isActiveDeco); } - private StatusFontIcon createForcedLabelMetadata(final Action action) { - StatusFontIcon result = null; + private StatusFontIcon createTypeLabelMetadata(final Action action) { if (ActionType.FORCED.equals(action.getActionType()) || ActionType.TIMEFORCED.equals(action.getActionType())) { - result = new StatusFontIcon(FontAwesome.BOLT, STATUS_ICON_FORCED, + return new StatusFontIcon(FontAwesome.BOLT, STATUS_ICON_FORCED, i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_FORCED), - UIComponentIdProvider.ACTION_HISTORY_TABLE_FORCED_LABEL_ID); + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); } - return result; + if (ActionType.SOFT.equals(action.getActionType())) { + return new StatusFontIcon(FontAwesome.STEP_FORWARD, STATUS_ICON_SOFT, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_SOFT), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + if (ActionType.DOWNLOAD_ONLY.equals(action.getActionType())) { + return new StatusFontIcon(FontAwesome.DOWNLOAD, STATUS_ICON_DOWNLOAD_ONLY, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_DOWNLOAD_ONLY), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + return null; } private StatusFontIcon createTimeForcedLabelMetadata(final Action action) { @@ -408,9 +407,6 @@ public class ActionHistoryGrid extends AbstractGrid { eventBus.publish(this, PinUnpinEvent.PIN_TARGET); } }); - if (!managementUIState.getDistributionTableFilters().getPinnedTarget().isPresent()) { - return; - } } // service call to cancel the active action @@ -443,7 +439,7 @@ public class ActionHistoryGrid extends AbstractGrid { @Override protected void setHiddenColumns() { - getColumn(VIRT_PROP_FORCED).setHidable(false); + getColumn(VIRT_PROP_TYPE).setHidable(false); getColumn(VIRT_PROP_TIMEFORCED).setHidable(false); getColumn(VIRT_PROP_ACTION_CANCEL).setHidable(false); getColumn(VIRT_PROP_ACTION_FORCE).setHidable(false); @@ -470,10 +466,8 @@ public class ActionHistoryGrid extends AbstractGrid { getColumn(ProxyAction.PXY_ACTION_STATUS).setHeaderCaption(i18n.getMessage("header.status")); getColumn(ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW) .setHeaderCaption(i18n.getMessage("header.maintenancewindow")); - getColumn(VIRT_PROP_FORCED).setHeaderCaption(String.valueOf(forceClientRefreshToggle)); - forceClientRefreshToggle = !forceClientRefreshToggle; - newHeaderRow.join(VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED).setText(i18n.getMessage("label.action.forced")); + newHeaderRow.join(VIRT_PROP_TYPE, VIRT_PROP_TIMEFORCED).setText(i18n.getMessage("label.action.type")); newHeaderRow.join(VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT) .setText(i18n.getMessage("header.action")); } @@ -485,7 +479,7 @@ public class ActionHistoryGrid extends AbstractGrid { setColumnsSize(100.0, 130.0, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT); setColumnsSize(53.0, 55.0, ProxyAction.PXY_ACTION_STATUS); setColumnsSize(150.0, 200.0, ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW); - setColumnsSize(FIXED_PIX_MIN, FIXED_PIX_MIN, VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, + setColumnsSize(FIXED_PIX_MIN, FIXED_PIX_MIN, VIRT_PROP_TYPE, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT); } @@ -569,7 +563,7 @@ public class ActionHistoryGrid extends AbstractGrid { protected GeneratedPropertyContainer addGeneratedContainerProperties() { final GeneratedPropertyContainer decoratedContainer = getDecoratedContainer(); - decoratedContainer.addGeneratedProperty(VIRT_PROP_FORCED, new GenericPropertyValueGenerator()); + decoratedContainer.addGeneratedProperty(VIRT_PROP_TYPE, new GenericPropertyValueGenerator()); decoratedContainer.addGeneratedProperty(VIRT_PROP_TIMEFORCED, new GenericPropertyValueGenerator()); decoratedContainer.addGeneratedProperty(VIRT_PROP_ACTION_CANCEL, new GenericPropertyValueGenerator()); decoratedContainer.addGeneratedProperty(VIRT_PROP_ACTION_FORCE, new GenericPropertyValueGenerator()); @@ -622,7 +616,7 @@ public class ActionHistoryGrid extends AbstractGrid { setColumnsSize(107.0, 500.0, ProxyAction.PXY_ACTION_DS_NAME_VERSION); setColumnsSize(100.0, 150.0, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT); setColumnsSize(53.0, 55.0, ProxyAction.PXY_ACTION_STATUS); - setColumnsSize(FIXED_PIX_MIN, FIXED_PIX_MAX, VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED, + setColumnsSize(FIXED_PIX_MIN, FIXED_PIX_MAX, VIRT_PROP_TYPE, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT); setColumnsSize(FIXED_PIX_MIN, 500.0, ProxyAction.PXY_ACTION_ROLLOUT_NAME); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java index 2b774659f..385066285 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java @@ -133,7 +133,7 @@ public class ActionStatusGrid extends AbstractGrid { } @Override - protected void addColumnRenderes() { + protected void addColumnRenderers() { getColumn(ProxyActionStatus.PXY_AS_STATUS).setRenderer(new HtmlLabelRenderer(), new HtmlStatusLabelConverter(this::createStatusLabelMetadata)); getColumn(ProxyActionStatus.PXY_AS_CREATED_AT).setConverter(new LongToFormattedDateStringConverter()); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusMsgGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusMsgGrid.java index 47d4bcbfb..43afb4c7e 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusMsgGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusMsgGrid.java @@ -146,7 +146,7 @@ public class ActionStatusMsgGrid extends AbstractGrid { } @Override - protected void addColumnRenderes() { + protected void addColumnRenderers() { // no specific column renderers } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTable.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTable.java index 095af4519..aa13acd43 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTable.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstable/DistributionTable.java @@ -8,6 +8,10 @@ */ package org.eclipse.hawkbit.ui.management.dstable; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.createAssignmentTab; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.isMaintenanceWindowValid; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.saveAllAssignments; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -18,27 +22,21 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; -import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetTagManagement; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent; -import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; -import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.DistributionSet; -import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.DistributionSetTagAssignmentResult; -import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; import org.eclipse.hawkbit.repository.model.Target; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.common.ConfirmationDialog; -import org.eclipse.hawkbit.ui.common.confirmwindow.layout.ConfirmationTab; import org.eclipse.hawkbit.ui.common.entity.DistributionSetIdName; import org.eclipse.hawkbit.ui.common.entity.TargetIdName; import org.eclipse.hawkbit.ui.common.table.AbstractNamedVersionTable; @@ -49,8 +47,6 @@ import org.eclipse.hawkbit.ui.management.event.DistributionTableEvent; import org.eclipse.hawkbit.ui.management.event.ManagementUIEvent; import org.eclipse.hawkbit.ui.management.event.PinUnpinEvent; import org.eclipse.hawkbit.ui.management.event.RefreshDistributionTableByFilterEvent; -import org.eclipse.hawkbit.ui.management.event.SaveActionWindowEvent; -import org.eclipse.hawkbit.ui.management.miscs.AbstractActionTypeOptionGroupLayout.ActionTypeOption; import org.eclipse.hawkbit.ui.management.miscs.ActionTypeOptionGroupAssignmentLayout; import org.eclipse.hawkbit.ui.management.miscs.MaintenanceWindowLayout; import org.eclipse.hawkbit.ui.management.state.ManagementUIState; @@ -66,8 +62,6 @@ import org.eclipse.hawkbit.ui.utils.UIMessageIdProvider; import org.eclipse.hawkbit.ui.utils.UINotification; import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; import org.eclipse.hawkbit.ui.view.filter.OnlyEventsFromDeploymentViewFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -86,13 +80,9 @@ import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; import com.vaadin.server.FontAwesome; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; -import com.vaadin.ui.CheckBox; import com.vaadin.ui.DragAndDropWrapper; -import com.vaadin.ui.HorizontalLayout; -import com.vaadin.ui.Link; import com.vaadin.ui.Table; import com.vaadin.ui.UI; -import com.vaadin.ui.themes.ValoTheme; /** * Distribution set table which is shown on the Deployment View. @@ -101,8 +91,6 @@ public class DistributionTable extends AbstractNamedVersionTable { - if (ok && isMaintenanceWindowValid()) { - saveAllAssignments(); + if (ok && isMaintenanceWindowValid(maintenanceWindowLayout, getNotification())) { + saveAllAssignments(managementUIState, actionTypeOptionGroupLayout, maintenanceWindowLayout, + deploymentManagement, getNotification(), getEventBus(), getI18n(), this); } else { managementUIState.getAssignedList().clear(); } - }, createAssignmentTab(), UIComponentIdProvider.DIST_SET_TO_TARGET_ASSIGNMENT_CONFIRM_ID); + }, createAssignmentTab(actionTypeOptionGroupLayout, maintenanceWindowLayout, saveButtonToggle(), + getI18n(), uiProperties), + UIComponentIdProvider.DIST_SET_TO_TARGET_ASSIGNMENT_CONFIRM_ID); + } + + private Consumer saveButtonToggle() { + return isEnabled -> confirmDialog.getOkButton().setEnabled(isEnabled); } private String createConfirmationQuestionForAssignment(final String distributionNameToAssign, @@ -489,145 +484,6 @@ public class DistributionTable extends AbstractNamedVersionTable itemIds = managementUIState.getAssignedList().keySet(); - Long distId; - List targetIdSetList; - List tempIdList; - final ActionType actionType = ((ActionTypeOption) actionTypeOptionGroupLayout.getActionTypeOptionGroup() - .getValue()).getActionType(); - final long forcedTimeStamp = (((ActionTypeOption) actionTypeOptionGroupLayout.getActionTypeOptionGroup() - .getValue()) == ActionTypeOption.AUTO_FORCED) - ? actionTypeOptionGroupLayout.getForcedTimeDateField().getValue().getTime() - : RepositoryModelConstants.NO_FORCE_TIME; - - final Map> saveAssignedList = Maps.newHashMapWithExpectedSize(itemIds.size()); - - for (final TargetIdName itemId : itemIds) { - final DistributionSetIdName distitem = managementUIState.getAssignedList().get(itemId); - distId = distitem.getId(); - - if (saveAssignedList.containsKey(distId)) { - targetIdSetList = saveAssignedList.get(distId); - } else { - targetIdSetList = new ArrayList<>(); - } - targetIdSetList.add(itemId); - saveAssignedList.put(distId, targetIdSetList); - } - - final String maintenanceSchedule = maintenanceWindowLayout.getMaintenanceSchedule(); - final String maintenanceDuration = maintenanceWindowLayout.getMaintenanceDuration(); - final String maintenanceTimeZone = maintenanceWindowLayout.getMaintenanceTimeZone(); - - for (final Map.Entry> mapEntry : saveAssignedList.entrySet()) { - tempIdList = saveAssignedList.get(mapEntry.getKey()); - final DistributionSetAssignmentResult distributionSetAssignmentResult = deploymentManagement - .assignDistributionSet(mapEntry.getKey(), - tempIdList.stream().map(t -> maintenanceWindowLayout.isEnabled() - ? new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp, - maintenanceSchedule, maintenanceDuration, maintenanceTimeZone) - : new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp)) - .collect(Collectors.toList())); - - if (distributionSetAssignmentResult.getAssigned() > 0) { - getNotification().displaySuccess(getI18n().getMessage("message.target.assignment", - distributionSetAssignmentResult.getAssigned())); - } - if (distributionSetAssignmentResult.getAlreadyAssigned() > 0) { - getNotification().displaySuccess(getI18n().getMessage("message.target.alreadyAssigned", - distributionSetAssignmentResult.getAlreadyAssigned())); - } - } - resfreshPinnedDetails(saveAssignedList); - - managementUIState.getAssignedList().clear(); - getNotification().displaySuccess(getI18n().getMessage("message.target.ds.assign.success")); - getEventBus().publish(this, SaveActionWindowEvent.SAVED_ASSIGNMENTS); - } - - private void resfreshPinnedDetails(final Map> saveAssignedList) { - final Optional pinnedDist = managementUIState.getTargetTableFilters().getPinnedDistId(); - final Optional pinnedTarget = managementUIState.getDistributionTableFilters().getPinnedTarget(); - - if (pinnedDist.isPresent()) { - if (saveAssignedList.keySet().contains(pinnedDist.get())) { - getEventBus().publish(this, PinUnpinEvent.PIN_DISTRIBUTION); - } - } else if (pinnedTarget.isPresent()) { - final Set assignedTargetIds = managementUIState.getAssignedList().keySet(); - if (assignedTargetIds.contains(pinnedTarget.get())) { - getEventBus().publish(this, PinUnpinEvent.PIN_TARGET); - } - } - } - - private ConfirmationTab createAssignmentTab() { - final ConfirmationTab assignmentTab = new ConfirmationTab(); - actionTypeOptionGroupLayout.selectDefaultOption(); - assignmentTab.addComponent(actionTypeOptionGroupLayout); - assignmentTab.addComponent(enableMaintenanceWindowLayout()); - initMaintenanceWindow(); - assignmentTab.addComponent(maintenanceWindowLayout); - return assignmentTab; - } - - private HorizontalLayout enableMaintenanceWindowLayout() { - final HorizontalLayout layout = new HorizontalLayout(); - layout.addComponent(enableMaintenanceWindowControl()); - layout.addComponent(maintenanceWindowHelpLinkControl()); - return layout; - } - - private CheckBox enableMaintenanceWindowControl() { - final CheckBox enableMaintenanceWindow = new CheckBox( - getI18n().getMessage("caption.maintenancewindow.enabled")); - enableMaintenanceWindow.setId(UIComponentIdProvider.MAINTENANCE_WINDOW_ENABLED_ID); - enableMaintenanceWindow.addStyleName(ValoTheme.CHECKBOX_SMALL); - enableMaintenanceWindow.addStyleName("dist-window-maintenance-window-enable"); - enableMaintenanceWindow.addValueChangeListener(event -> { - final Boolean isMaintenanceWindowEnabled = enableMaintenanceWindow.getValue(); - maintenanceWindowLayout.setVisible(isMaintenanceWindowEnabled); - maintenanceWindowLayout.setEnabled(isMaintenanceWindowEnabled); - enableSaveButton(!isMaintenanceWindowEnabled); - maintenanceWindowLayout.clearAllControls(); - }); - return enableMaintenanceWindow; - } - - private Link maintenanceWindowHelpLinkControl() { - final String maintenanceWindowHelpUrl = uiProperties.getLinks().getDocumentation().getMaintenanceWindowView(); - return SPUIComponentProvider.getHelpLink(getI18n(), maintenanceWindowHelpUrl); - } - - private void initMaintenanceWindow() { - maintenanceWindowLayout.setVisible(false); - maintenanceWindowLayout.setEnabled(false); - maintenanceWindowLayout.getScheduleControl() - .addTextChangeListener(event -> enableSaveButton(maintenanceWindowLayout.onScheduleChange(event))); - maintenanceWindowLayout.getDurationControl() - .addTextChangeListener(event -> enableSaveButton(maintenanceWindowLayout.onDurationChange(event))); - } - - private void enableSaveButton(final boolean enabled) { - confirmDialog.getOkButton().setEnabled(enabled); - } - @Override protected List hasMissingPermissionsForDrop() { return permissionChecker.hasUpdateTargetPermission() ? Collections.emptyList() diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AbstractActionTypeOptionGroupLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AbstractActionTypeOptionGroupLayout.java index ddbc4ca7d..53efaa1ab 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AbstractActionTypeOptionGroupLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AbstractActionTypeOptionGroupLayout.java @@ -78,9 +78,25 @@ public abstract class AbstractActionTypeOptionGroupLayout extends HorizontalLayo softLabel.setCaption(i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_SOFT)); softLabel.setDescription(i18n.getMessage(UIMessageIdProvider.TOOLTIP_SOFT_ITEM)); softLabel.setStyleName("padding-right-style"); + softLabel.setIcon(FontAwesome.STEP_FORWARD); addComponent(softLabel); } + protected void addDownloadOnlyItemWithLabel() { + final FlexibleOptionGroupItemComponent downloadOnlyItem = actionTypeOptionGroup + .getItemComponent(ActionTypeOption.DOWNLOAD_ONLY); + downloadOnlyItem.setId(UIComponentIdProvider.ACTION_DETAILS_DOWNLOAD_ONLY_ID); + downloadOnlyItem.setStyleName(STYLE_DIST_WINDOW_ACTIONTYPE); + addComponent(downloadOnlyItem); + final Label downloadOnlyLabel = new Label(); + downloadOnlyLabel.setSizeFull(); + downloadOnlyLabel.setCaption(i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_DOWNLOAD_ONLY)); + downloadOnlyLabel.setDescription(i18n.getMessage(UIMessageIdProvider.TOOLTIP_DOWNLOAD_ONLY_ITEM)); + downloadOnlyLabel.setStyleName("padding-right-style"); + downloadOnlyLabel.setIcon(FontAwesome.DOWNLOAD); + addComponent(downloadOnlyLabel); + } + /** * To Set Default option for save. */ @@ -93,7 +109,8 @@ public abstract class AbstractActionTypeOptionGroupLayout extends HorizontalLayo * */ public enum ActionTypeOption { - FORCED(ActionType.FORCED), SOFT(ActionType.SOFT), AUTO_FORCED(ActionType.TIMEFORCED); + FORCED(ActionType.FORCED), SOFT(ActionType.SOFT), AUTO_FORCED(ActionType.TIMEFORCED), + DOWNLOAD_ONLY(ActionType.DOWNLOAD_ONLY); private final ActionType actionType; diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAssignmentLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAssignmentLayout.java index 6773ef522..36837db56 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAssignmentLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAssignmentLayout.java @@ -70,13 +70,17 @@ public class ActionTypeOptionGroupAssignmentLayout extends AbstractActionTypeOpt actionTypeOptionGroup.addItem(ActionTypeOption.SOFT); actionTypeOptionGroup.addItem(ActionTypeOption.FORCED); actionTypeOptionGroup.addItem(ActionTypeOption.AUTO_FORCED); + actionTypeOptionGroup.addItem(ActionTypeOption.DOWNLOAD_ONLY); selectDefaultOption(); addForcedItemWithLabel(); addSoftItemWithLabel(); addAutoForceItemWithLabelAndDateField(); + addDownloadOnlyItemWithLabel(); } + + private void addAutoForceItemWithLabelAndDateField() { final FlexibleOptionGroupItemComponent autoForceItem = actionTypeOptionGroup .getItemComponent(ActionTypeOption.AUTO_FORCED); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAutoAssignmentLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAutoAssignmentLayout.java index d7b518524..c12da1282 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAutoAssignmentLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/ActionTypeOptionGroupAutoAssignmentLayout.java @@ -32,9 +32,11 @@ public class ActionTypeOptionGroupAutoAssignmentLayout extends AbstractActionTyp actionTypeOptionGroup = new FlexibleOptionGroup(); actionTypeOptionGroup.addItem(ActionTypeOption.SOFT); actionTypeOptionGroup.addItem(ActionTypeOption.FORCED); + actionTypeOptionGroup.addItem(ActionTypeOption.DOWNLOAD_ONLY); selectDefaultOption(); addForcedItemWithLabel(); addSoftItemWithLabel(); + addDownloadOnlyItemWithLabel(); } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java index cc48ea94f..a424b595b 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTable.java @@ -8,6 +8,10 @@ */ package org.eclipse.hawkbit.ui.management.targettable; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.createAssignmentTab; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.isMaintenanceWindowValid; +import static org.eclipse.hawkbit.ui.management.TargetAssignmentOperations.saveAllAssignments; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -19,6 +23,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -26,32 +31,24 @@ import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.FilterParams; -import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetTagManagement; import org.eclipse.hawkbit.repository.event.remote.entity.RemoteEntityEvent; -import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; -import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.DistributionSet; -import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; -import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; import org.eclipse.hawkbit.repository.model.Tag; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.model.TargetTagAssignmentResult; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; -import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.common.ConfirmationDialog; import org.eclipse.hawkbit.ui.common.ManagementEntityState; import org.eclipse.hawkbit.ui.common.UserDetailsFormatter; -import org.eclipse.hawkbit.ui.common.confirmwindow.layout.ConfirmationTab; import org.eclipse.hawkbit.ui.common.entity.DistributionSetIdName; import org.eclipse.hawkbit.ui.common.entity.TargetIdName; import org.eclipse.hawkbit.ui.common.table.AbstractTable; import org.eclipse.hawkbit.ui.common.table.BaseEntityEventType; -import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; import org.eclipse.hawkbit.ui.dd.criteria.ManagementViewClientCriterion; import org.eclipse.hawkbit.ui.management.event.ManagementUIEvent; import org.eclipse.hawkbit.ui.management.event.PinUnpinEvent; @@ -60,7 +57,6 @@ import org.eclipse.hawkbit.ui.management.event.TargetAddUpdateWindowEvent; import org.eclipse.hawkbit.ui.management.event.TargetFilterEvent; import org.eclipse.hawkbit.ui.management.event.TargetTableEvent; import org.eclipse.hawkbit.ui.management.event.TargetTableEvent.TargetComponentEvent; -import org.eclipse.hawkbit.ui.management.miscs.AbstractActionTypeOptionGroupLayout.ActionTypeOption; import org.eclipse.hawkbit.ui.management.miscs.ActionTypeOptionGroupAssignmentLayout; import org.eclipse.hawkbit.ui.management.miscs.MaintenanceWindowLayout; import org.eclipse.hawkbit.ui.management.state.ManagementUIState; @@ -99,11 +95,8 @@ import com.vaadin.server.FontAwesome; import com.vaadin.shared.ui.label.ContentMode; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; -import com.vaadin.ui.CheckBox; import com.vaadin.ui.DragAndDropWrapper; -import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; -import com.vaadin.ui.Link; import com.vaadin.ui.Table; import com.vaadin.ui.UI; import com.vaadin.ui.themes.ValoTheme; @@ -379,8 +372,7 @@ public class TargetTable extends AbstractTable { if ( isFilteredByTags()) { - final List list = new ArrayList<>(); - list.addAll(managementUIState.getTargetTableFilters().getClickedTargetTags()); + final List list = new ArrayList<>(managementUIState.getTargetTableFilters().getClickedTargetTags()); queryConfig.put(SPUIDefinitions.FILTER_BY_TAG, list.toArray(new String[list.size()])); } if (isFilteredByStatus()) { @@ -524,7 +516,7 @@ public class TargetTable extends AbstractTable { } private void tagAssignment(final DragAndDropEvent event) { - final List targetList = getDraggedTargetList(event).stream().collect(Collectors.toList()); + final List targetList = new ArrayList<>(getDraggedTargetList(event)); final String targTagName = HawkbitCommonUtil.removePrefix(event.getTransferable().getSourceComponent().getId(), SPUIDefinitions.TARGET_TAG_ID_PREFIXS); @@ -858,95 +850,6 @@ public class TargetTable extends AbstractTable { return !managementUIState.getTargetTableFilters().getClickedTargetTags().isEmpty(); } - // Code for assignment start - private void saveAllAssignments() { - final Set itemIds = managementUIState.getAssignedList().keySet(); - Long distId; - List targetIdSetList; - List tempIdList; - final ActionType actionType = ((ActionTypeOption) actionTypeOptionGroupLayout.getActionTypeOptionGroup() - .getValue()).getActionType(); - final long forcedTimeStamp = (((ActionTypeOption) actionTypeOptionGroupLayout.getActionTypeOptionGroup() - .getValue()) == ActionTypeOption.AUTO_FORCED) - ? actionTypeOptionGroupLayout.getForcedTimeDateField().getValue().getTime() - : RepositoryModelConstants.NO_FORCE_TIME; - - final Map> saveAssignedList = Maps.newHashMapWithExpectedSize(itemIds.size()); - - for (final TargetIdName itemId : itemIds) { - final DistributionSetIdName distitem = managementUIState.getAssignedList().get(itemId); - distId = distitem.getId(); - - if (saveAssignedList.containsKey(distId)) { - targetIdSetList = saveAssignedList.get(distId); - } else { - targetIdSetList = new ArrayList<>(); - } - targetIdSetList.add(itemId); - saveAssignedList.put(distId, targetIdSetList); - } - - final String maintenanceSchedule = maintenanceWindowLayout.getMaintenanceSchedule(); - final String maintenanceDuration = maintenanceWindowLayout.getMaintenanceDuration(); - final String maintenanceTimeZone = maintenanceWindowLayout.getMaintenanceTimeZone(); - - for (final Map.Entry> mapEntry : saveAssignedList.entrySet()) { - tempIdList = saveAssignedList.get(mapEntry.getKey()); - final DistributionSetAssignmentResult distributionSetAssignmentResult = deploymentManagement - .assignDistributionSet(mapEntry.getKey(), - tempIdList.stream().map(t -> maintenanceWindowLayout.isEnabled() - ? new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp, - maintenanceSchedule, maintenanceDuration, maintenanceTimeZone) - : new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp)) - .collect(Collectors.toList())); - - if (distributionSetAssignmentResult.getAssigned() > 0) { - getNotification().displaySuccess(getI18n().getMessage("message.target.assignment", - distributionSetAssignmentResult.getAssigned())); - } - if (distributionSetAssignmentResult.getAlreadyAssigned() > 0) { - getNotification().displaySuccess(getI18n().getMessage("message.target.alreadyAssigned", - distributionSetAssignmentResult.getAlreadyAssigned())); - } - } - resfreshPinnedDetails(saveAssignedList); - - managementUIState.getAssignedList().clear(); - getNotification().displaySuccess(getI18n().getMessage("message.target.ds.assign.success")); - getEventBus().publish(this, SaveActionWindowEvent.SAVED_ASSIGNMENTS); - } - - private void resfreshPinnedDetails(final Map> saveAssignedList) { - final Optional pinnedDist = managementUIState.getTargetTableFilters().getPinnedDistId(); - final Optional pinnedTarget = managementUIState.getDistributionTableFilters().getPinnedTarget(); - - if (pinnedDist.isPresent()) { - if (saveAssignedList.keySet().contains(pinnedDist.get())) { - getEventBus().publish(this, PinUnpinEvent.PIN_DISTRIBUTION); - } - } else if (pinnedTarget.isPresent()) { - final Set assignedTargetIds = managementUIState.getAssignedList().keySet(); - if (assignedTargetIds.contains(pinnedTarget.get())) { - getEventBus().publish(this, PinUnpinEvent.PIN_TARGET); - } - } - } - - private boolean isMaintenanceWindowValid() { - if (maintenanceWindowLayout.isEnabled()) { - try { - MaintenanceScheduleHelper.validateMaintenanceSchedule(maintenanceWindowLayout.getMaintenanceSchedule(), - maintenanceWindowLayout.getMaintenanceDuration(), - maintenanceWindowLayout.getMaintenanceTimeZone()); - } catch (final InvalidMaintenanceScheduleException e) { - LOG.error("Maintenance window is not valid", e); - getNotification().displayValidationError(e.getMessage()); - return false; - } - } - return true; - } - private void assignDsToTarget(final DragAndDropEvent event) { final TableTransferable transferable = (TableTransferable) event.getTransferable(); final AbstractTable source = (AbstractTable) transferable.getSourceComponent(); @@ -988,65 +891,21 @@ public class TargetTable extends AbstractTable { getI18n().getMessage(MESSAGE_CONFIRM_ASSIGN_ENTITY, distributionNameToAssign, "target", targetName), getI18n().getMessage(UIMessageIdProvider.BUTTON_OK), getI18n().getMessage(UIMessageIdProvider.BUTTON_CANCEL), ok -> { - if (ok && isMaintenanceWindowValid()) { - saveAllAssignments(); + if (ok && isMaintenanceWindowValid(maintenanceWindowLayout, getNotification())) { + saveAllAssignments(managementUIState, actionTypeOptionGroupLayout, maintenanceWindowLayout, + deploymentManagement, getNotification(), getEventBus(), getI18n(), this); } else { managementUIState.getAssignedList().clear(); } - }, createAssignmentTab(), UIComponentIdProvider.DIST_SET_TO_TARGET_ASSIGNMENT_CONFIRM_ID); + }, createAssignmentTab(actionTypeOptionGroupLayout, maintenanceWindowLayout, saveButtonToggle(), + getI18n(), uiProperties), + UIComponentIdProvider.DIST_SET_TO_TARGET_ASSIGNMENT_CONFIRM_ID); UI.getCurrent().addWindow(confirmDialog.getWindow()); confirmDialog.getWindow().bringToFront(); } - private ConfirmationTab createAssignmentTab() { - final ConfirmationTab assignmentTab = new ConfirmationTab(); - actionTypeOptionGroupLayout.selectDefaultOption(); - assignmentTab.addComponent(actionTypeOptionGroupLayout); - assignmentTab.addComponent(enableMaintenanceWindowLayout()); - initMaintenanceWindow(); - assignmentTab.addComponent(maintenanceWindowLayout); - return assignmentTab; - } - - private HorizontalLayout enableMaintenanceWindowLayout() { - final HorizontalLayout layout = new HorizontalLayout(); - layout.addComponent(enableMaintenanceWindowControl()); - layout.addComponent(maintenanceWindowHelpLinkControl()); - return layout; - } - - private CheckBox enableMaintenanceWindowControl() { - final CheckBox enableMaintenanceWindow = new CheckBox( - getI18n().getMessage("caption.maintenancewindow.enabled")); - enableMaintenanceWindow.setId(UIComponentIdProvider.MAINTENANCE_WINDOW_ENABLED_ID); - enableMaintenanceWindow.addStyleName(ValoTheme.CHECKBOX_SMALL); - enableMaintenanceWindow.addStyleName("dist-window-maintenance-window-enable"); - enableMaintenanceWindow.addValueChangeListener(event -> { - final Boolean isMaintenanceWindowEnabled = enableMaintenanceWindow.getValue(); - maintenanceWindowLayout.setVisible(isMaintenanceWindowEnabled); - maintenanceWindowLayout.setEnabled(isMaintenanceWindowEnabled); - enableSaveButton(!isMaintenanceWindowEnabled); - maintenanceWindowLayout.clearAllControls(); - }); - return enableMaintenanceWindow; - } - - private Link maintenanceWindowHelpLinkControl() { - final String maintenanceWindowHelpUrl = uiProperties.getLinks().getDocumentation().getMaintenanceWindowView(); - return SPUIComponentProvider.getHelpLink(getI18n(), maintenanceWindowHelpUrl); - } - - private void initMaintenanceWindow() { - maintenanceWindowLayout.setVisible(false); - maintenanceWindowLayout.setEnabled(false); - maintenanceWindowLayout.getScheduleControl() - .addTextChangeListener(event -> enableSaveButton(maintenanceWindowLayout.onScheduleChange(event))); - maintenanceWindowLayout.getDurationControl() - .addTextChangeListener(event -> enableSaveButton(maintenanceWindowLayout.onDurationChange(event))); - } - - private void enableSaveButton(final boolean enabled) { - confirmDialog.getOkButton().setEnabled(enabled); + private Consumer saveButtonToggle() { + return isEnabled -> confirmDialog.getOkButton().setEnabled(isEnabled); } private void addNewTargetToAssignmentList(final TargetIdName createTargetIdName, diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java index b5fd2c5b9..754506494 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/targettable/TargetTableLayout.java @@ -36,10 +36,6 @@ public class TargetTableLayout extends AbstractTableLayout { private final transient EventBus.UIEventBus eventBus; - private final TargetDetails targetDetails; - - private final TargetTableHeader targetTableHeader; - public TargetTableLayout(final UIEventBus eventBus, final TargetTable targetTable, final TargetManagement targetManagement, final EntityFactory entityFactory, final VaadinMessageSource i18n, final UINotification uiNotification, final ManagementUIState managementUIState, @@ -50,10 +46,10 @@ public class TargetTableLayout extends AbstractTableLayout { final TargetMetadataPopupLayout targetMetadataPopupLayout = new TargetMetadataPopupLayout(i18n, uiNotification, eventBus, targetManagement, entityFactory, permissionChecker); this.eventBus = eventBus; - this.targetDetails = new TargetDetails(i18n, eventBus, permissionChecker, managementUIState, uiNotification, + TargetDetails targetDetails = new TargetDetails(i18n, eventBus, permissionChecker, managementUIState, uiNotification, tagManagement, targetManagement, targetMetadataPopupLayout, deploymentManagement, entityFactory, targetTable); - this.targetTableHeader = new TargetTableHeader(i18n, permissionChecker, eventBus, uiNotification, + TargetTableHeader targetTableHeader = new TargetTableHeader(i18n, permissionChecker, eventBus, uiNotification, managementUIState, managementViewClientCriterion, targetManagement, deploymentManagement, uiProperties, entityFactory, uiNotification, tagManagement, distributionSetManagement, uiExecutor, targetTable); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java index e44ffc9d9..e92e1c32d 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java @@ -392,6 +392,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { private CommonDialogWindow createWindow() { return new WindowBuilder(SPUIDefinitions.CREATE_UPDATE_WINDOW) .caption(i18n.getMessage("caption.create.new", i18n.getMessage("caption.rollout"))).content(this) + .id(UIComponentIdProvider.ROLLOUT_POPUP_ID) .layout(this).i18n(i18n).helpLink(uiProperties.getLinks().getDocumentation().getRolloutView()) .saveDialogCloseListener(new SaveOnDialogCloseListener()).buildCommonDialogWindow(); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java index e1a6a3b01..0409c3a5d 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/ProxyRollout.java @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.ui.rollout.rollout; +import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; import org.eclipse.hawkbit.ui.customrenderers.client.renderers.RolloutRendererData; @@ -45,6 +46,15 @@ public class ProxyRollout { private TotalTargetCountStatus totalTargetCountStatus; private String approvalDecidedBy; private String approvalRemark; + private ActionType actionType; + + public ActionType getActionType() { + return actionType; + } + + public void setActionType(final ActionType actionType) { + this.actionType = actionType; + } public RolloutRendererData getRolloutRendererData() { return rolloutRendererData; diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java index fa0fa04e0..5d0ad67a9 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutBeanQuery.java @@ -118,6 +118,7 @@ public class RolloutBeanQuery extends AbstractBeanQuery { proxyRollout.setForcedTime(rollout.getForcedTime()); proxyRollout.setId(rollout.getId()); proxyRollout.setStatus(rollout.getStatus()); + proxyRollout.setActionType(rollout.getActionType()); proxyRollout.setRolloutRendererData(new RolloutRendererData(rollout.getName(), rollout.getStatus().toString())); final TotalTargetCountStatus totalTargetCountActionStatus = rollout.getTotalTargetCountStatus(); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java index 841f21c8a..d3df87de5 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java @@ -26,6 +26,7 @@ import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; @@ -38,6 +39,7 @@ import org.eclipse.hawkbit.ui.common.ConfirmationDialog; import org.eclipse.hawkbit.ui.common.grid.AbstractGrid; import org.eclipse.hawkbit.ui.customrenderers.client.renderers.RolloutRendererData; import org.eclipse.hawkbit.ui.customrenderers.renderers.AbstractGridButtonConverter; +import org.eclipse.hawkbit.ui.customrenderers.renderers.AbstractHtmlLabelConverter; import org.eclipse.hawkbit.ui.customrenderers.renderers.GridButtonRenderer; import org.eclipse.hawkbit.ui.customrenderers.renderers.HtmlLabelRenderer; import org.eclipse.hawkbit.ui.customrenderers.renderers.RolloutRenderer; @@ -87,6 +89,10 @@ public class RolloutListGrid extends AbstractGrid { private static final String VIRT_PROP_UPDATE = "update"; private static final String VIRT_PROP_COPY = "copy"; private static final String VIRT_PROP_DELETE = "delete"; + private static final String STATUS_ICON_DOWNLOAD_ONLY = "statusIconDownloadOnly"; + private static final String STATUS_ICON_SOFT = "statusIconSoft"; + private static final String STATUS_ICON_FORCED = "statusIconForced"; + private static final String PROP_TYPE = "actionType"; private final transient RolloutManagement rolloutManagement; @@ -100,6 +106,8 @@ public class RolloutListGrid extends AbstractGrid { private final RolloutUIState rolloutUIState; + private final AlignCellStyleGenerator alignGenerator; + private static final List DELETE_COPY_BUTTON_ENABLED = Arrays.asList(RolloutStatus.CREATING, RolloutStatus.ERROR_CREATING, RolloutStatus.ERROR_STARTING, RolloutStatus.PAUSED, RolloutStatus.READY, RolloutStatus.RUNNING, RolloutStatus.STARTING, RolloutStatus.STOPPED, RolloutStatus.FINISHED, @@ -109,7 +117,7 @@ public class RolloutListGrid extends AbstractGrid { RolloutStatus.ERROR_CREATING, RolloutStatus.ERROR_STARTING, RolloutStatus.PAUSED, RolloutStatus.READY, RolloutStatus.RUNNING, RolloutStatus.STARTING, RolloutStatus.STOPPED); - private static final List PAUSE_BUTTON_ENABLED = Arrays.asList(RolloutStatus.RUNNING); + private static final List PAUSE_BUTTON_ENABLED = Collections.singletonList(RolloutStatus.RUNNING); private static final List RUN_BUTTON_ENABLED = Arrays.asList(RolloutStatus.READY, RolloutStatus.PAUSED); @@ -119,11 +127,13 @@ public class RolloutListGrid extends AbstractGrid { private static final Map statusIconMap = new EnumMap<>(RolloutStatus.class); - private static final List HIDDEN_COLUMNS = Arrays.asList(SPUILabelDefinitions.VAR_CREATED_DATE, + private static final List HIDDEN_COLUMNS = Arrays.asList(PROP_TYPE, SPUILabelDefinitions.VAR_CREATED_DATE, SPUILabelDefinitions.VAR_CREATED_USER, SPUILabelDefinitions.VAR_MODIFIED_DATE, SPUILabelDefinitions.VAR_MODIFIED_BY, SPUILabelDefinitions.VAR_APPROVAL_DECIDED_BY, SPUILabelDefinitions.VAR_APPROVAL_REMARK, SPUILabelDefinitions.VAR_DESC); + private static final String[] centerAlignedColumns = new String[] { PROP_TYPE }; + static { statusIconMap.put(RolloutStatus.FINISHED, new StatusFontIcon(FontAwesome.CHECK_CIRCLE, SPUIStyleDefinitions.STATUS_ICON_GREEN)); @@ -163,6 +173,7 @@ public class RolloutListGrid extends AbstractGrid { rolloutGroupManagement, quotaManagement); this.uiNotification = uiNotification; this.rolloutUIState = rolloutUIState; + alignGenerator = new AlignCellStyleGenerator(null, centerAlignedColumns, null); setGeneratedPropertySupport(new RolloutGeneratedPropertySupport()); init(); @@ -183,7 +194,7 @@ public class RolloutListGrid extends AbstractGrid { refreshContainer(); break; default: - return; + break; } } @@ -231,6 +242,7 @@ public class RolloutListGrid extends AbstractGrid { private void updateItem(final Rollout rollout, final Item item) { final TotalTargetCountStatus totalTargetCountStatus = rollout.getTotalTargetCountStatus(); item.getItemProperty(SPUILabelDefinitions.VAR_STATUS).setValue(rollout.getStatus()); + item.getItemProperty(PROP_TYPE).setValue(rollout.getActionType()); item.getItemProperty(SPUILabelDefinitions.VAR_TOTAL_TARGETS_COUNT_STATUS).setValue(totalTargetCountStatus); final Long groupCount = Long .valueOf((Integer) item.getItemProperty(SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS).getValue()); @@ -270,6 +282,7 @@ public class RolloutListGrid extends AbstractGrid { false); rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_APPROVAL_DECIDED_BY, String.class, null, false, false); + rolloutGridContainer.addContainerProperty(PROP_TYPE, ActionType.class, null, false, false); rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_APPROVAL_REMARK, String.class, null, false, false); rolloutGridContainer.addContainerProperty(SPUILabelDefinitions.VAR_MODIFIED_DATE, String.class, null, false, @@ -307,6 +320,9 @@ public class RolloutListGrid extends AbstractGrid { getColumn(SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS).setMinimumWidth(40); getColumn(SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS).setMaximumWidth(60); + getColumn(PROP_TYPE).setMinimumWidth(45); + getColumn(PROP_TYPE).setMaximumWidth(45); + getColumn(VIRT_PROP_RUN).setMinimumWidth(25); getColumn(VIRT_PROP_RUN).setMaximumWidth(25); @@ -335,6 +351,7 @@ public class RolloutListGrid extends AbstractGrid { .setHeaderCaption(i18n.getMessage("header.distributionset")); getColumn(SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS).setHeaderCaption(i18n.getMessage("header.numberofgroups")); getColumn(SPUILabelDefinitions.VAR_TOTAL_TARGETS).setHeaderCaption(i18n.getMessage("header.total.targets")); + getColumn(PROP_TYPE).setHeaderCaption(i18n.getMessage("header.type")); getColumn(SPUILabelDefinitions.VAR_CREATED_DATE).setHeaderCaption(i18n.getMessage("header.createdDate")); getColumn(SPUILabelDefinitions.VAR_CREATED_USER).setHeaderCaption(i18n.getMessage("header.createdBy")); getColumn(SPUILabelDefinitions.VAR_MODIFIED_DATE).setHeaderCaption(i18n.getMessage("header.modifiedDate")); @@ -374,13 +391,14 @@ public class RolloutListGrid extends AbstractGrid { final List columnsToShowInOrder = Arrays.asList(ROLLOUT_RENDERER_DATA, SPUILabelDefinitions.VAR_DIST_NAME_VERSION, SPUILabelDefinitions.VAR_STATUS, SPUILabelDefinitions.VAR_TOTAL_TARGETS_COUNT_STATUS, SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS, - SPUILabelDefinitions.VAR_TOTAL_TARGETS, VIRT_PROP_APPROVE, VIRT_PROP_RUN, VIRT_PROP_PAUSE, + SPUILabelDefinitions.VAR_TOTAL_TARGETS, PROP_TYPE, VIRT_PROP_APPROVE, VIRT_PROP_RUN, VIRT_PROP_PAUSE, VIRT_PROP_UPDATE, VIRT_PROP_COPY, VIRT_PROP_DELETE, SPUILabelDefinitions.VAR_CREATED_DATE, SPUILabelDefinitions.VAR_CREATED_USER, SPUILabelDefinitions.VAR_MODIFIED_DATE, SPUILabelDefinitions.VAR_MODIFIED_BY, SPUILabelDefinitions.VAR_APPROVAL_DECIDED_BY, SPUILabelDefinitions.VAR_APPROVAL_REMARK, SPUILabelDefinitions.VAR_DESC); setColumns(columnsToShowInOrder.toArray()); + setCellStyleGenerator(alignGenerator); } @Override @@ -403,7 +421,7 @@ public class RolloutListGrid extends AbstractGrid { } @Override - protected void addColumnRenderes() { + protected void addColumnRenderers() { getColumn(SPUILabelDefinitions.VAR_NUMBER_OF_GROUPS).setRenderer(new HtmlRenderer(), new TotalTargetGroupsConverter()); getColumn(SPUILabelDefinitions.VAR_TOTAL_TARGETS_COUNT_STATUS).setRenderer(new HtmlRenderer(), @@ -411,6 +429,9 @@ public class RolloutListGrid extends AbstractGrid { getColumn(SPUILabelDefinitions.VAR_STATUS).setRenderer(new HtmlLabelRenderer(), new RolloutStatusConverter()); + getColumn(PROP_TYPE).setRenderer(new HtmlLabelRenderer(), + new RolloutTypeConverter(this::createTypeLabelAdapter)); + final RolloutRenderer customObjectRenderer = new RolloutRenderer(RolloutRendererData.class); customObjectRenderer.addClickListener(this::onClickOfRolloutName); getColumn(ROLLOUT_RENDERER_DATA).setRenderer(customObjectRenderer); @@ -465,7 +486,7 @@ public class RolloutListGrid extends AbstractGrid { @Override public LazyQueryContainer getRawContainer() { - return (LazyQueryContainer) (getDecoratedContainer()).getWrappedContainer(); + return (LazyQueryContainer) getDecoratedContainer().getWrappedContainer(); } @Override @@ -550,7 +571,6 @@ public class RolloutListGrid extends AbstractGrid { if (RolloutStatus.PAUSED.equals(rolloutStatus)) { rolloutManagement.resumeRollout(rolloutId); uiNotification.displaySuccess(i18n.getMessage("message.rollout.resumed", rolloutName)); - return; } } @@ -608,7 +628,7 @@ public class RolloutListGrid extends AbstractGrid { } final Long runningActions = statusTotalCount.get(Status.RUNNING); String rolloutDetailsMessage = ""; - if ((scheduledActions > 0) || (runningActions > 0)) { + if (scheduledActions > 0 || runningActions > 0) { rolloutDetailsMessage = i18n.getMessage("message.delete.rollout.details", runningActions, scheduledActions); } @@ -716,6 +736,51 @@ public class RolloutListGrid extends AbstractGrid { } } + /** + * + * Converter to convert {@link RolloutStatus} to string. + * + */ + + class RolloutTypeConverter extends AbstractHtmlLabelConverter { + + private static final long serialVersionUID = 1L; + + RolloutTypeConverter(final LabelAdapter adapter) { + addAdapter(adapter); + } + + @Override + public Class getModelType() { + return ActionType.class; + } + + } + + private StatusFontIcon createTypeLabelAdapter(final ActionType actionType) { + if (ActionType.FORCED.equals(actionType)) { + return new StatusFontIcon(FontAwesome.BOLT, STATUS_ICON_FORCED, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_FORCED), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + if (ActionType.TIMEFORCED.equals(actionType)) { + return new StatusFontIcon(FontAwesome.HISTORY, STATUS_ICON_FORCED, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_TIME_FORCED), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + if (ActionType.SOFT.equals(actionType)) { + return new StatusFontIcon(FontAwesome.STEP_FORWARD, STATUS_ICON_SOFT, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_SOFT), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + if (ActionType.DOWNLOAD_ONLY.equals(actionType)) { + return new StatusFontIcon(FontAwesome.DOWNLOAD, STATUS_ICON_DOWNLOAD_ONLY, + i18n.getMessage(UIMessageIdProvider.CAPTION_ACTION_DOWNLOAD_ONLY), + UIComponentIdProvider.ACTION_HISTORY_TABLE_TYPE_LABEL_ID); + } + return null; + } + /** * Converter to convert {@link TotalTargetCountStatus} to formatted string * with status and count details. @@ -782,7 +847,7 @@ public class RolloutListGrid extends AbstractGrid { } } - private final void hideColumnsDueToInsufficientPermissions() { + private void hideColumnsDueToInsufficientPermissions() { final List modifiableColumnsList = getColumns().stream().map(Column::getPropertyId) .collect(Collectors.toList()); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgroup/RolloutGroupListGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgroup/RolloutGroupListGrid.java index 4327b4d33..c90d09ecc 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgroup/RolloutGroupListGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgroup/RolloutGroupListGrid.java @@ -233,7 +233,7 @@ public class RolloutGroupListGrid extends AbstractGrid { } @Override - protected void addColumnRenderes() { + protected void addColumnRenderers() { getColumn(SPUILabelDefinitions.VAR_STATUS).setRenderer(new HtmlLabelRenderer(), new RolloutGroupStatusConverter()); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgrouptargets/RolloutGroupTargetsListGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgrouptargets/RolloutGroupTargetsListGrid.java index b6882c74e..662db3373 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgrouptargets/RolloutGroupTargetsListGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rolloutgrouptargets/RolloutGroupTargetsListGrid.java @@ -12,6 +12,7 @@ import java.util.EnumMap; import java.util.Locale; import java.util.Map; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus; @@ -173,7 +174,7 @@ public class RolloutGroupTargetsListGrid extends AbstractGrid Action.ActionType.DOWNLOAD_ONLY.equals(group.getRollout().getActionType())) + .orElse(false); + + return isFinishedDownloadOnlyAssignment ? statusIconMap.get(Status.FINISHED) : statusIconMap.get(status); + } + private String getStatus() { final RolloutGroup rolloutGroup = rolloutUIState.getRolloutGroup().orElse(null); if (rolloutGroup != null && rolloutGroup.getStatus() == RolloutGroupStatus.READY) { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java index 408a36427..2c34c3fde 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java @@ -177,6 +177,10 @@ public final class UIComponentIdProvider { * Action history table cancel Id. */ public static final String ACTION_DETAILS_SOFT_ID = "action.details.soft.group"; + /** + * Download Only Action Id. + */ + public static final String ACTION_DETAILS_DOWNLOAD_ONLY_ID = "action.details.downloadonly.group"; /** * Start type of rollout manual radio button */ @@ -256,7 +260,7 @@ public final class UIComponentIdProvider { /** * Action history table forced label Id. */ - public static final String ACTION_HISTORY_TABLE_FORCED_LABEL_ID = "action.history.table.forcedId"; + public static final String ACTION_HISTORY_TABLE_TYPE_LABEL_ID = "action.history.table.typeId"; /** * Action history table time-forced label Id. @@ -1145,6 +1149,11 @@ public final class UIComponentIdProvider { */ public static final String METADATA_POPUP_ID = "metadata.popup.id"; + /** + * Rollout popup id. + */ + public static final String ROLLOUT_POPUP_ID = "add.update.rollout.popup"; + /** * DistributionSet table details tab id in Distributions . */ diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java index 2efa408a5..05e874c9f 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIMessageIdProvider.java @@ -31,6 +31,8 @@ public final class UIMessageIdProvider { public static final String CAPTION_ACTION_SOFT = "label.action.soft"; + public static final String CAPTION_ACTION_DOWNLOAD_ONLY = "label.action.downloadonly"; + public static final String CAPTION_ACTION_TIME_FORCED = "label.action.time.forced"; public static final String CAPTION_ACTION_MESSAGES = "caption.action.messages"; @@ -129,6 +131,8 @@ public final class UIMessageIdProvider { public static final String TOOLTIP_SOFT_ITEM = "tooltip.soft.item"; + public static final String TOOLTIP_DOWNLOAD_ONLY_ITEM = "tooltip.downloadonly.item"; + public static final String TOOLTIP_FORCED_ITEM = "tooltip.forced.item"; public static final String TOOLTIP_TARGET_PIN = "tooltip.target.pin"; diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties index 37bffcf5b..d138cddc3 100644 --- a/hawkbit-ui/src/main/resources/messages.properties +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -165,7 +165,9 @@ label.active = Active label.action.id = Action Id label.no.tag = NO TAG label.action.forced = Forced +label.action.type = Type label.action.soft = Soft +label.action.downloadonly = Download Only label.action.time.forced = Time Forced label.dist.details.type = Type : label.dist.details.name = Name : @@ -218,6 +220,7 @@ label.cancelled = Cancelled label.cancelling = Canceling label.retrieved = Retrieved label.download = Downloading +label.downloaded = Downloaded label.unknown = Unknown label.target.id = Controller Id : label.target.ip = Controller IP : @@ -283,6 +286,7 @@ tooltip.status.overdue = Overdue tooltip.delete.module = Select and delete Software Module tooltip.forced.item=Device is supposed to install the update immediately tooltip.soft.item=Device can execute the update at any time, e.g. with user approval or according to its regular update time plan +tooltip.downloadonly.item=Device is supposed to only download the update and not install it tooltip.timeforced.item=Soft update which turns into a forced update after a specific time tooltip.timeforced.forced.in=Auto forcing in {0} tooltip.timeforced.forced.since=Auto forced since {0} @@ -409,6 +413,7 @@ message.forcequit.action = Force Quit.. message.forcequit.action.success = Action has been force quit successfully ! message.forcequit.action.failed = Force Quitting the action is not possible ! message.forcequit.action.confirm = Attention!\nForce quit should only be used when the assignment action is not working properly.\nForce quitting an action has no effect on the connected target. It is just resetting \nthe data stored on the SP update server. \nAre you absolutely sure you want to force quit this action? +message.downloadonly.action = DownloadOnly message.distribution.no.update = distribution {0} set is already assigned to targets and cannot be changed message.action.not.allowed = Action not allowed message.action.did.not.work = Action did not work. Please try again. @@ -570,6 +575,7 @@ header.name = Name header.vendor = Vendor header.version = Version header.description = Description +header.type = Type header.createdBy = Created By header.createdDate = Created Date header.modifiedBy = Modified By