From 78558f9c8e327bb65ec606507778ac2632917ad6 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 20 Mar 2020 11:37:35 +1300 Subject: [PATCH 01/40] chore(version): bump version number --- api/portainer.go | 2 +- api/swagger.yaml | 4 ++-- api/swagger_config.json | 2 +- distribution/portainer.spec | 2 +- package.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/portainer.go b/api/portainer.go index e97230a0e..23b149c82 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -917,7 +917,7 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "1.23.2" + APIVersion = "1.24.0-dev" // DBVersion is the version number of the Portainer database DBVersion = 22 // AssetsServerURL represents the URL of the Portainer asset server diff --git a/api/swagger.yaml b/api/swagger.yaml index dd980a700..0d0a60a89 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -54,7 +54,7 @@ info: **NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8). - version: "1.23.2" + version: "1.24.0-dev" title: "Portainer API" contact: email: "info@portainer.io" @@ -3174,7 +3174,7 @@ definitions: description: "Is analytics enabled" Version: type: "string" - example: "1.23.2" + example: "1.24.0-dev" description: "Portainer API version" PublicSettingsInspectResponse: type: "object" diff --git a/api/swagger_config.json b/api/swagger_config.json index dc70a5211..3bdb70688 100644 --- a/api/swagger_config.json +++ b/api/swagger_config.json @@ -1,5 +1,5 @@ { "packageName": "portainer", - "packageVersion": "1.23.2", + "packageVersion": "1.24.0-dev", "projectName": "portainer" } diff --git a/distribution/portainer.spec b/distribution/portainer.spec index 93cad801f..6b215cad7 100644 --- a/distribution/portainer.spec +++ b/distribution/portainer.spec @@ -1,5 +1,5 @@ Name: portainer -Version: 1.23.2 +Version: 1.24.0-dev Release: 0 License: Zlib Summary: A lightweight docker management UI diff --git a/package.json b/package.json index f0f72ce8b..9cc2882da 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "1.23.2", + "version": "1.24.0-dev", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git" From ae7f46c8ef32c10e527fa8e88b3b5e304ca5ebff Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 26 Mar 2020 07:44:27 +0200 Subject: [PATCH 02/40] feat(endpoints): filter by endpoint type (#3646) --- api/http/handler/endpoints/endpoint_list.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index b89899d16..403a3f668 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -28,6 +28,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true) limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true) + endpointType, _ := request.RetrieveNumericQueryParameter(r, "type", true) endpointGroups, err := handler.EndpointGroupService.EndpointGroups() if err != nil { @@ -54,6 +55,10 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, search) } + if endpointType != 0 { + filteredEndpoints = filterEndpointsByType(filteredEndpoints, portainer.EndpointType(endpointType)) + } + filteredEndpointCount := len(filteredEndpoints) paginatedEndpoints := paginateEndpoints(filteredEndpoints, start, limit) @@ -156,3 +161,14 @@ func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGrou return false } + +func filterEndpointsByType(endpoints []portainer.Endpoint, endpointType portainer.EndpointType) []portainer.Endpoint { + filteredEndpoints := make([]portainer.Endpoint, 0) + + for _, endpoint := range endpoints { + if endpoint.Type == endpointType { + filteredEndpoints = append(filteredEndpoints, endpoint) + } + } + return filteredEndpoints +} From 00bef100eefffe5280de27eb5f9e602f34010962 Mon Sep 17 00:00:00 2001 From: Ben Brooks Date: Fri, 27 Mar 2020 03:49:20 +0000 Subject: [PATCH 03/40] chore(assets): double UI image resolutions for HiDPI displays (#3648) Fixes #3069 Prevents users seeing blurry logos and other images when using a hidpi display (like scaled 4k, or a Retina display). These images have been recreated manually with 2x the original resolution but should resemble the originals as much as possible. They have also been run through pngcrush for compression. --- assets/images/logo.png | Bin 1769 -> 5074 bytes assets/images/logo_small.png | Bin 1092 -> 3105 bytes assets/images/support_1.png | Bin 4733 -> 5200 bytes assets/images/support_2.png | Bin 5753 -> 6291 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/images/logo.png b/assets/images/logo.png index f0ba92c53e07910bbc3e0817016d2b3a10be51bf..2e46594f291f5e2e3541c3c8fdd240afb7b70217 100644 GIT binary patch literal 5074 zcmV;@6D{nCP)mtBEed0vA7j0;bsI*fDsW1wqiRCW6)lz@FjMG5fKUWV%r-(pcZaw z(2#o6!A%aHLpqFz1R51vHeN+O+?3#TpfOdr9{<430$htI7!gsi`CANb!BW`y3kSpC z9yFvDyTag7%z~X^I3C7dL`21QDn`T36kO2FJ+@>#iVbj6jW4YN6cH7hPwa*7Tg}nd z-~lA|VnKHYe2#Ls$-%Rlzlo^Wye%8AAP;Uz@gcVLz{!In(658}TO^F9h=__U1~+2~ z?EH;GJba`o0+(SH>4cx4? z=4ie;1izy_f7AY(NNDrs9PQ;+bF|HPtq($X?{?Ei zSPVP$7=hRE7T)cJ;nrn+jdy$Db-a(oXq^#ww;e`ALbce!H%3H4x7cz#s20V@v6P}3 zIUOj4n|$kX++$WbUSdQfbc-#^gO=b{WLciWL}Yc~X=}!IG_tI(lmn3EB}PQLLnjXw z;xZV^!x+=eU(nqX1`DiRI3ptB?_#?G#_|Zpc5~fh?^eb34~%#ge?gup4$qCQN{e z$MJXsmm&?uYa|(G<58T6B6n1!%^rpcP?uVJlQnq9YrBaOfk#n+ zhLqv1a4)u~t?Zq1kmX7rhHGbynOS=;*0%TBwr$&3UE8*8+qP}ryEX@3|M8@%>#a;q zaFTP*x7}6mA}2{B-B07!z<6+|(aM(2pb*t74zM|4Jo(4Sb7vKCSf3$`w&lEIDt=H(8htSsKEB8m3dDu;;`yg@;@52O~o&##zyZN)sWWJ zOmJ_wB7?0r5c8bYvh5t$9N1!Lw9!XK(>T#=|5W{R+M4NB(^!_c;fmfB?GXUBnGS3Y zYzfqb0yZdpznm~AZ6rHYmo(UhuqfS6^H+N@Aoq6nYKPHwuYjpY0*I9Soo#f2(VkrIkdH};yrTnM_`QryG&+mF_vU&WzgiMM zFOF@k3|Yb9H=7zAM!YJlajFe?ZX3Rf=kB!LxgPfo3{wcmu$_$7W?=smTK*F`Cw2&3 z-^OBnTgzyD{OT6qM>D>ucW>{&$)((jI$B_CL!5SEuGX34_Y(={7^C*UA;~uRt=Wim zTk`hl$QOH z`CQh}9tDgj_`w2*%d9wV$g_Qc^yiilXTFs&58!^D#iWl}pKJDeZ_5#mX81XoV-*+r zMUwe)_me+b8-Q7li1?1m3oSm^pj7?MP zkrQx1op0mo>Zbna86{otz zMjyeN0aWZo4YsW#_>(5smg?j}GSO3YWVGe{T_eA}OAQ+(;aiXn@8|GK-G|ihEkgJ) zUY^|)KfjUd)nu-Y)=tG$mUP3=D$!j5VW{MN-}}60IZ{8Ca3)1>U_00P?Z_YtofxGq z47hi&ZJ7t=r{I^wm4YkZopie*0(2Sx->)8jfSl&lm zG;W6{0NeT!&b>lb=4087r#eCz`-!l&OdD+H27v4K9L9s~iUe#+tT27d!7{&F*OpVe z{RV(uCSE^ztq5sV9U1Z*hvqc9=ZRmljD9wH55L~!|Fh)Gi1~CKTSNd`H7$|Z-v0^n zKh%nkctZdlaM8z70sKX{-nVpmKv(5rqbh;cbm00H@%)Hz%(XK+{%lPAJ$FP9*rYXh zK@ixic-46M2)7iN{6H9AqQ(3**OyCvo)Ti*t&9Two(&hi<-b&{uh4S;l8Q!W4m<_y zbB(>dVLkJn97evF^#6{yZJia^XgF*yWz5Tme^F__Ot`)yt(Qo8T}5jz9k9_txx-I1 zF^>=UeQ*(iZKgDd=TY+=%6Lm9wM{t%Xdg2CT!GUeLETb*J5mb!JTl*b#H#|oD(J%P zPo|$IANd&W3j;Q7;(tlka13K_I<8fT|EtIeU*~bX(o(ZJ;C+dNcP1@|ZRt`s!>=}k zW1`U)QlNh)yy<{V9BKSGUDNJ_u@T|y%Jo;}**RW*4A1}@o>YoI)2dvZ!rB;hX3Xja zqyOx@g5O=jg3Sg9K)am^d}ZQRKMSzo&!%=anD6`B!k~h ztrOL+o29j|Z%D9Bv(hb1u$?VwdO}Q#@x#P33$RU(u`K8bX^~deaSY{;wK&ppK-Bc0 zYdTLV&nCIRM*2|P>63d3@N zjXc@Cz+gK|D!OfA`d7i2oacTH%3BU>I)0R&{lNZ_cyyMg^b-mFdPLH!YY4C%oB`Oz zO09Zs!gxNK5!fci2-_Y48o$=?bu#oYk~EE@ZKMTzTnMmzK&4tJur(A*@b?jiLR3@4 zAhNato5;tMyjH>wJxOY&A<`VLku*r+*Rk?}$Ah~t}b3I+fly9(|6#%ZAC2`fA9I(5` zyaIu3v3K%S0_`nvSy!xEw%aYCUpvbCHVXl^jWPh+(E{Aj3FEnCMqt||M%cC!unpJN z3SFnP+!GTmgE6! z`$hoU?g{!7ZRZrg)=0osL--ctNiT!|+rvc$wo8f`Y-L!w zE>fY(j4loM{R+r^k&5dh3(^%$z60p#c1D-Q;lpHJS5iyWiPG&IT_j-BmSoboQ^%Dm z*w72o6}^`7kw}!GLrVZ`t!3)ZLu$#4sEx(X!}1BXb!yXf&tv7w+IVn5!mVOn67zps znbi5W#IZpFzn+#kJ)MgLY$F61mnKNlhFE+{8EjX`q*N-q6jX#@Bd@+Lmdw@(wn;iy zD)b5Y1l!#ygN;m*IoN*GZE+%KB;$iYQ!5^jiNGUc`ZY|9FOLX33JJERMxRKUc8D3z zainP}gKc9O@V|({zCwTvO_bq@-D6gE?To&W=~)%|0$V#~GN~vfv`R}AY%OKgz;vTg5es>L0}W zHAx9<^>l;Eq+lbhXUm2YonlPbK2-pDPCi#2tXZ3ld$vGeTf%&=@(7?gMr!3}{R%R` zd|5uu2@(7%SHD<1-@M4c)>*bGn#&TPCK2{4yO?K78f<7^+b_V=Jq$d6%qa5r3+Oq% zZN9P)H)>Ko6=C2D?>di=M!uPWCXTviBM+71LRGloM_dI z_3GHpxt&g3p*6c&fS+p_{U;!u62h|@T)tqHb z*U5R6B@H&>^|Vw-F9WP{Kfuvltz;?Hmr@zN%y0PwTNyoy?+NF0rgSyQ6WCVu%BsMP z9aMZL5jP_L?J1Mn7pUPRG1w@`S-8Kcxw8V>EOrp6ku73&V;V?%t2?kNnOaXCpB-|d z=fOS`lwV?fX)p1s2bRX?br)B$2o)RH%2>$0Kp=iAErPC83~d>M8^U7o&jl7g0Q1rW z8{#{DBC9g~pi&!28nh)%I?*$_NLndXbZh&iFviw_@Tl7`A=vc(zod+PN_iM4Y0%AR z6DB4v5d+K>fIqKbJ2eZcQNy>+5Nsc?1I151XP*87cpju1@SU_IstE5<5yml&r5394 zF<`uaUoY_g8u4oe(>=
O*}UCP-Khb7GKdJ6OZ>pE@=%-*I&I1TE@s@uaY002H?8;${aSDEKg&j$wRWB z8Z~wE48is(*Cx<9nnPv{a#hr*?=uDvr(k@BPkY|?T&l%a(M zT6olvnP+-?#^L|GC|iR^0Nb)Wf~_hS*wD)C>M~slZcutmCp~)djwoA$KN9kp&Q#zv z(&gJYpI}2{DPyy8l`-jNhBc8NFzLgJ_a8bS$2iVUR#+zm+}odnRJb?RI@-;w=YNlI`h0ziDUUJ;J_~p+Li4%y z2ls3n@vxvrtZ>~F;kn%b$P8N0m*%i$uOay>sKQlM@GW1)ui3=g%lC81Ctt8VQUA10 zsp>?;BRewiE0}5bqW``wR--#)fbeYfo30)%9k6NQ6TXv}F@6h$^Ej@z%Jad*>+|l5 zSZ{kVAlZPHOPpi<23x9#IT^U0^^Oh+B@Fv>?A-M%Y(Ed<%dnVp-g;Q!S~=pg{Q=bKA*{CL zlD}HT$)n6yJd$|dM1DAh<#e4hJ69BlL~#o!tUWwn@Iluiv-aY8JAgKeg!S7w;#UCJ zmuugo2sS4trxI1XgIq#7T2v#gb%f6XrfL~S_?Vi@8!KrXum^gDH3e9@dBxBBSKPMhOGhaBgHL8x__O*s_{{)VCTT$*^VB9jWgiiXbEzwk+!5 z8B*Rglt)N1Y+0>G%DaJzh&&|u=fN$cx+5rpkYw2MXon|Aamx^r3|k>u;}TNZ3p7GV zGHivajiq>q6t)r}$*}zj_0S8mumW?i5`S|0Q63@5u>DTkQOM3?AF^&Usv;!)ZicNo z4k6E;peagVBJSW@JB~j1A0bJ_nDbMq9*3YN8kBVI!7cB-)}BLeigQ*nkok zjWc+Q&vyg!Q45g_Nro+h(rAG}n2I52i%N*}NiuAbWY{Fhut}0(lVsQ=$*@V1VUr}o oCP{`(l3|l1!zM|FO_F}6_wZymPGo`c5C8xG07*qoM6N<$f|jAd*Z=?k literal 1769 zcmVN zc8UbEW}2EIEq*lkt3gpjRtGi5bykN2kx*FJAD{lXci(;Q-TPkdrZmj#`@A!E_nz~d z^Pcm4&-*;*dCqPKsicxhDygK>InmeIj!Zdp7TfU{opR_bwhM4o%As>B?Qt1-+zBa< z(rnsdd%S5j7I@QMOY}>*6o)p$+dguOvW#uh$1Ofq??T;DI)#yqv=+;KG1nzH&T_2t z<&;rAvH|YZ>;-2Q_N2zzsKtGb>gu5S7@EC?S!AaVR5>Xs8)`hxk9{Ae%1%2iGR#nA zT@~ASpG3xVRc!r@j;;yWpAW}Ar(?FaEQ{&2>mHsOwsw@uW1ksMT4zbh<&eQMF2WxU${2dvRq7~;cl39#>X5?L&qY3W zkXGAi&iP-OU8`QbI(Mj7Z_S~I?NGF3)nK;=!E2UUYppwzu<5SS$svT`DCe2t^95JO zSeF>7N35);O2b0X(;3Pgu2L;#sx%^|HPBR3Gq%PlDl_4nBUCy*t8u!D5cF2zGT$_$ z!@YZ{aEY(x?kZI{Jp|pHlD};-LS@c4dnk9Yu?L93^ReY+K{{eP41aX2by~Cgw>523 z3Lyk#?$VU4+3vfs7=#t3*@1<|#P)w{vl>IJ%a*iSZJB(QC90!?>NV{Nb_Nb zMfd1tzTMfHy>8Y$s%fxAg*Wh6-u&0#i0Ipe-p{sg_LCwTdehXZ^?)nXs&z+6*tU32 zs}<&&YbACW73EG|z)Nm*nIGGZyJOgP;1vsuaI{PP3Aeh`#1KL-#%`@1b(MJ@)2hL% z3D}yk#f>gAUwE<&oCY6h)L^l3$=EE#c1vBR%8Pg@am`t%MT-wT zphb%&B(c)#Ti*_+Y**d7Mn8rJ(pK^3#J!tHbIRVi%eIe#v3KQ*beuW zy(*)+QFe72+dMqgW_6YMr424EgiVDjBahpTZ3>?8*=P@V2OdnyLL|wGBG|V2z*(RDqnY<=kdGf z0miWDZ#Ql$uy1=1^=@#MSyp4K&lJX1o`|iwXq0fGS0c?9bHbzk9*%1E_(5)zP%I`j z(#uiHO@<^#35BtRpoi)xLfPS0?fyOoTQBe7>Vi?iJbR)s)Fwa+L3cYMwI=r)Yn-!8 zwZXIy4AN8>TL`)t?n>WxT0El5>I`y$8%>J;{YYC(8Y>yBGIs;2htth5$EhKN;0%+K zV$z&@RmStoj&`o=otZnTOiUVkgq4Xg=@BNn!3=|o$E5dXq3!bx3;ZW*x#sGtpXp{9 zsY-Q6Y+XH@Tjr_EOX>EUmvlq4Tx$(Xv2_G@j1}%x5oI(w%A+Z^j@bIR#!9ajJ=`0e zV#|d#L|qAIqSwVt`!&w(>d?CTM@dJ?>5*R!woxUXz|Txs#j(vP5!*v4t2nj`OT@N4 zKdsZXu?^N-BDj-NT5)hSB|&S<&9J4|dbzEH6}K!U)e+r5Q(WP>98htTucZIfkqqxB zuSTF6oaWnBd)Kq7^-1Xz12@3sR$ApI$E7qwT_9^G00000 LNkvXXu0mjf25yMr diff --git a/assets/images/logo_small.png b/assets/images/logo_small.png index 636f43ab9b15ccb74ca6dd92c6d6c1949b68f657..76d3a46b09e32632c0a74475028a6ac2add3fcb1 100644 GIT binary patch literal 3105 zcmV++4BqpJP)aaYhOC%4T@H{4bvpVfjEKEfh5IT zn69860zkepkPk)2D46b}9YR8Uk94xI1STT)IdBW_`-Fv;64_9;WCL47b!6S&tO`PTu5Rgo}+Xi-k*4q(t$MniY5R* z18Xr5P04}Hw?xa}`P#gs3XSg?>jxOhYC74sodDn23%D z;td0dkHR+DkqU)vvf#skY=YS6KwRrTgv5Uk2m2Et5E$YO0~rK+XR#di4r4v+ZS(7a z02^Vd08!37#p(xp$FKC ze(>71ZQHhOo7c8&+qP}nw(Wy&&*E#6-zHDy?as{Z% zvwY7toTev_k)9ib`}$dS6Wm1)_cDh08j;U8dGCIQV*;Nj6(da_S_F`A#9dc$a`s}zxeaog%4MTuCnr!yXK|%f#E~cBIIJ{8 z=?Rj zV9mYG+^fr!c(En#dHlbW`x!R6jXK;b)5`msm1;5t(!F~Z&mB~{88|m{?U2jOsc-ZA zE?~*93h0{QxUiPzPCTsF_ytH~DmbcD?B12vmg(R14hZo_FhIMJ(J*_#M&dPF!ZQJS z?Pqjx(L?s5L4+x+F-ilN5(YW#xSLTa!zyt?agc$aVQ(*eRa7KnDUhI|ecXO9A;{Q*A!`C6&F zP(7t0mTNVcuKXzUmd}9Xld*#gnb)wb{tO!a2l&UsG_0Gg#vR`-AkG>}H;OpUVc4Fj z6yZD1S<@rzuOOh6r7@f-M$I$$e=g7ds&png)zZ@~r(tdc%8SJNS1A1fumzNX7eK;5 z`zzDbGtk*epONPXF8j>?j2u8fL;IMKBAV8#Xcvbln~54j`fdSN5HQ}sE$!C1@DA_q zwF1Kb3n|Z?I7cpx=94^sj~yKXk}Ev{!f+(RMtVybR^YXgVX>cp5DqbiJ;hpq0B{FW zz%Y$S+7lm8IU(lUr)uKJrof_*%%bvjfP|Pg6Hh{3tY%^adXTAUh7@^y21p2=0QpV# z9m=n~I0h}JJ6RlL3sb&#^1Lps?@`Y|=KWegi02j=j}L&iUxFw!#AH|bOHwJ`D${S3p*2fVVc_|IxVbk=I3ed}fzc8his`o1Ps5kbgzT;s+p?EA8Q48M}5u`iqLG z+SPsnvOzO~EN4rF40HShM9w+wL<88!JAVr!_$b%jb`giPHCo01q`Jp-kT1ory&%GMy z1wyN+1bJ;C3x)F9G@k*P<+cz7zzv0K7CXkZJGcq&jEVtBQICL>7bC6Po^+H)K$_Ua zk1e;IXJ3$0L9P|x={w>m0v&+_BuG2B&SyZ13WrFxn}u9qBX&+05d7+&L`4E$k=K}p zyk@!fiqC+|;Qx3UQe*yqg8%cBZgLq%BiU)+LxPUhh4n$O`?GO7V#E3 z`2~n*@L3Lx@XAW4ASK&KL+m(6ss88LjE0C`Xt@9)^9 zm9jn!;h~BT5aQ2@6OgNgO;L#V?aK2vVY6JHC?I`30!$?4L1 zxLV@_gnDFZe}-E?P)o{l8=a#rjoG-VH}h=!MBTg?F`fb*1@C|e@5dHgjb}%DDhl#( z90N>S7(Ec{6@!W65DReU%Q&dOA;gg*Jzl;7g0P1+Qc(+8m^@RY=}cCR*O2Z&$^k*F zI`0+^lc!`_#bV<&u}k6B1OT~*{N~emPl^MOYk4<}GwBU$d14zc6AQ{WCJIOv29K9O zLHGj5Ft%z0gmK%H-fAdzQILvxPZt-9fRjX=3t0%Qsnn18Ylw9IYw!Mb1;|`GK+uao zU!asr9_vyzQ%O&Ntu1|z7a-_E+`vIPkYSX66MldrOg=UPdqx6)l#LKD_OFR&s|j}R z2GS9rL#vO*QTL>gJjM+OB0?_#1JpYpG=K|e&XF1;i zXR(jb17-?9f*8q(0MbDg01dP?Do2mSJ||bYgK1V{(p#0nw>^kM1NlJg^|I6RJbRtX zgFsKQTe&4^D#_GFo~HYO?e#jyK47Y8z4s9JXt*(T3e%Dg92PH6rS~l_>Yya{VYc0N zcRHVU99HyS!id(ba9>XP-g2n#EXtxJooY+U_DPXOZ-7uCK^+wkSppLOq?a1aYVD^~ z_Dhxof5P>PiKAHpfTT=DMvjXqI!_kRd=1Gvb@VWoz8Xl{0?z=YI`qO{Lw9-tVq^e8 z*JAX@(3a^#AYvc(>P8?MeHC25=?jRF0R-KTYitb$&aD`;UPJA)2gJw#B8=>(F|sO0 z{0Tt}P6J3<%K-8LE=wA#pTT|$w)&aLE@N{e-Nzh_{Hm0ZN*NhBKEUzJP$XG_ZSmGQ|p0%(K_^&TRhvzNH4oA`~+{q zLn9(&Jc-?Spm4@mhP&P9ki&j-L4*l@vEKndxrqopY)b%mNA)#2n+33Ei30EZ$w1+)gFCi(ml-abA*iT zUUiZR^?tCh6l5zlVvmn-XGb1M+aH^&3ImdU?_?{SQ5++l@K`$Qp;C}^=>1CZ7&eQ3Tu=%4hn zsYgmdp7wA$YsFuHjEcY&258XWBz@&N1sUZ?s=1t^WMO>Cc{Hi)AVyA;l`+lXhzKv+ zD5IBZ+pO#aq#hGxWSmnHBke&JYgVC`oYkI+XazCA5}6bv>q{@H(o2mdON#w5t^{Pa zTsx4fvCBStjY#~k3SVPo;Z!J$L|AHl(mK~~c5BvRK}ptUj*w{+a}DXd-Y!wurj8^T%iyGroLs105r(?3!@_C~wzxC#$X9mQZ%>;{ zMP0evvF@=zsT@(Ag4CA>#v&cdtYmy%tIPk(7&oluer=~9E6WRHc&8u-%j#7a z;e}2>c9h$@u!k{b+u07JKFU$#ys6peW*K6!`DKdHC0)`b?c`5F1d8URvP=m80000< KMNUMnLSTY2=M!51 diff --git a/assets/images/support_1.png b/assets/images/support_1.png index e33bc088b2d64a4e052323aed9b5d07e124f6937..9b92a10f755871f5325f53d01c6dd891703e972c 100644 GIT binary patch literal 5200 zcmV-W6tC-vP))P(NyWKwaw?EeYwxtC^GBbAosZ?#XYt~w|q9`OYb7v+z6+{ul zXK}5zf>stSh=_|71tgg}_s(R7gh%8}07*!~%kT*AydU5G&zZ}RkPHD6B{TE=f4{>e zbMM@__x|rW-}%mWzVj&(5)u*;5)u-R&BzjBse?aS$HD z|Hozd0^d?3`X&ZxEC0&j3FX zpC7Wbm;8lY3qI}e7PdlbyCpKqrHjF117hHq0x<|OBv)r*hxtq~+;8$RnK)*UbqsB@ z+#uP~aFE>qjSqn6U#X+!e_4_E?4YXV7i^y3t87}e#AX z7AuMliuF5c#I`*rL{)XI*nhZAlvbP+8+RWU`RhIrfBc|aO#R~)amQmt0>2YGhbzeD z972|&#GwW6q?b&Q=zTam`V5epkl2iz0Bhhcr$)tZ-`XkGmez<xej(`|us#?>2}h-q#{YJJ7c!{{J#4 zCKEOD^=;y|sgbzJhR};6#tC{`ITZQoV1MG0GVud#2_VhulWMHXg{ax zRU|HfVE3y2v4?NO+uD&6!t5AXV2uT_tXrOts;>Kq93{^H>z5Au|+OxRraH+#m=fz@iz9^MLS4S z6A;^LL^GL_=4=uNkJbyq;o}V=cXkQA0G$FU&T+-~zkViaKb}&65g6+5Xdl@DV)YpFb2~jmG}ZQ`kv!`V6tM_+Xr5c<}LS z;TO`aFH|JD1|H75_T*+kJ5{g2?JS{W=P@y2TqtgmFNdH>z;)^Dy%Y8xsujO@IKnTY zHK0+6{_^(}iH<<9{|59iThZorFpF<|SRnvzdw(`D%nuU#E~{i&BnQ@43qp9)VJ6<@ zj%%@zJ$?I?ibT6OvnLo-{gL=XB(vxFpO3hYd-OyDcUuN`*4)df$!tw_RGkz6N6;*% zi~Grlc#TpdVo*H=KStN0#nN8Z1pFpE#htBhM)z%bzZR+lK&j%h(rcHlJk zH_`6KLyq%T>=A^m7!{iM^2T-}KfTi~+uT;amkEuBFY#NO#GecIi|U$s@d@N+e1_lb zFuuI0?6^2n-%MR8-WMDB(#I?5j_K3I71T9CL?~|d@?P2?2(uUOj1!0l$Dzsgz(YmI zvXn=W5I$@db7m)qAG{^}H?)z@g=6V=;&Ez=Om0(u4Go=)pvJuAd#KOb={E_BgH`mM zPW}SnrKKO!J`fxg$PHVdq&rlX`4v2f%2>)xkhRC$aEP@9gY)r$g++l%kpMi3RJ`q| zc=i1<*0#H67PFRtFgE8m zh}iZ+mfW2yhvkWxXxxuGSV4JXN;+f2gw0pCC)fr}d-#>jq(yjWtc^@qZGDTlNbtZw z4Kxp-MUHyyJ6uQRp0%;Qv4uySdWv0pPl;iElPp7Ai^pSVkud7Xwefzjs_DbioZ$Rx zku6b;@MOI0`|*SEfdk&^CDo!Y;&t@ValP@j7=*LK^L$ETEZ20YCN;b5^T zo%zOgnAvjinpDc%Ft$v>8qH?fYm^Vo3;v^$ZlKW? zn--eo@R}7vT!wh?*>&fJ085YZLxVHKgYSJAaKuKwO*+Nrpvo<+&78v2Z#r0Qe!qA#NjLTNo#UzyW?EKnjJymjsq zAHXQnBQ|a2*U}`3F8F+G+%!niiQx4+;{}Hqhm<0?p|J&dTHO-N!m}nk`lJ*d)Hmj) zHNhNn*B7U*iPnNIr!&EvZtF1usXTJ@NV*e@Z7_u+SAm-_AfC1aTf?I7rg)G;KY##7 zS`us}6+Whu}ImZ>zY&|A3cXsnIuU%-;R`)a({hw_gF4Fm zGP+K0tA5I>3dZYckoKued`PHy>}$NFq%+j% z<`?X4^IlZXslzZL4gT{C{3HTTtGRJPC_e3UP)-o!7LoSu9;yYuqexJkI`|G}clZtD zAcw^vj0GnX9-X(n7uH8#fI-{6+35rB#3_*K^anqWNTtm2@FQJr}B=DkJ zDPswbc-5*N(0^q?(fhh=GapXkHjiDy%g$32K=GjJib7Zr)_rjrVqPP)wD ziwdXL;LOFoh#i$Dxd05B6KZ!k=W6(vz+Wp8-NQDf@ar(uyRtlC_NYtoF#8Jj9jfKN zE}~cLRB=&MZ>UCf75t|naq+O{1b*o7g%4%ALsTZA*GNY9i#>GYTtlj<90SLY-^Bc< zh542A8BcL(kK?BsMQGz8@zlanF$wdZUjFlLjw#CEt5JM(^A%9lGYP@&W8ZpGh zb+Q`mF8wY=qQ@j3v}uI`DL>**t14(<;!`<9m|@sm5tsP95*<0wz~-5Yt#~5>O~*kb5dKV&=siG#zlMJ10!sNAoNKVDeQkNUs5#kq2}$bB zs5x07roFZWvXw`CFwvZyrn&ExQ9I0St);O%Yk!HcSKP)UTsp7+pnAh0yDRYTio|CF zs$c_cu5$QDn{|5lT;sX&6V~WnbHo+}sxaffn6=#IjSAHlI)T{oBb2b9Nc38bhb#JL zFS(Mo=TCugc#S8KFtFe5i!=@z8*Cfq4{=@Sw~Z>Xt^5Slf95Hkjm@pBku9wq1)uRg z9K-E1+FH!^-^VipZ$#LU&Izm0gh7L2w6e=^skOaZ#{UQR%b7yfAIxFSCS9s`)@ z^DxXE`95auJ&jdZc4NJTx*_gRGZEwTg~XlHi_zw87B9Z@F-;2NDzydct4a9(OGEp_ z9CUXu3lMXX*J2DhgxP?)$Z(s@I9I*HWA25ecm>0K1G~DuVJN*w*GR*iio_)li$$(z zfh(s!yd1-Br`0^p-Ks|0=kA|vensh`2)F85aUIO(T}T6Z3*XrTb9x5OQ2h{hxM^^X z*@E%eR>Zg4GIDiJEP%8P&LJ^cGF+i%n-*!XqnbOBs(L~n4Anxp!zV|-)rCp9A|tQp zsw{8tHgfuKjrmw`>yCe$wCoyHl|)|%{pi8jG~*@^X~6UfXi_bkthRxcfG>b0DO}k$Bl0CLPDr ztMH~Vb3Svxm=FGvOytEZeMjv!Y2|Q`ILxR2169E3wj@9H6KVFmqU-Jc@N@X>BZ!fo zzX%r*%_*^XITzZ;7JN@?vju8bvuLp^Z+MrrKvnle%ec}y9c$J>iHdGo&-Vi$QUiIxm*3sRdEp z5SxhT2%AV+<_?*QFK43dwtJyLsJ}&;N53&(%qw3c(#WKD5PU}3Pef#7FS&}_{&C@N z!eJYUb3Kb1BAZd%yUpQCKx_iRy>NUQ-+>*4iCtP1gG*DyQ^S&NtF z4eGZ#ypenCuHXpO709AB3LRrV%*dkuyL2bo;{R5C=6HDGb5)NS0xA3P;D zny4FuHnYv<3T_xNzHrO5*S6Kpd9SP~8D#qW(z=n83%7&p^&qn?k>f=fDyh487QEIh zkNyHiV-kAAHZHGZ6t7r&M0~ve6lYzmDec>WW$(B!X>dags^f-=ZEoW*Jo+y05%*5_ zI431dwZrw{Pk3QR%}nSRA&au&t{Fw8zkj!!3wv7CX8W@6H9CC;D$$pT`=4G@fr4n2 zxR-=4F}!swho{?8(6KW9PxVDF==mbNSTWQWEVX$;@iM(hAl9{-G+4108d_x$oG=oH z?FvlDN}WiTEoaq0gccj{26sy^CPepK65LL>UZ)oL zO$wyE2Ij#$OM*L_X|6n*^f71L^i~ii3GOIX_%gVQMzNB2DyTd7&&Z%S1t&`qe7^8A zG3ste=9NmFTA@RYktGT4(oBbG5aOpvye6T3H-_OP!3iYxY|PP0vVzv`ixt2&x)suN zo2FMLJ;7SA3ionR_e{?QNd{AsxF)4OW!QvuXHa8zX5Gi(h+P~opMUo#l2kAJ=3#6 z^8ELebOKEDdQ2`$R)TkPGM_~15{yhvIWbKVED4qbOM)fAl3+=&Bv=wG36=y)f+fL{ zU1iK7v)wbJoiw!QHQ$WbgtPT-2oJ z(mgpwkHrC@ml7}MIxegxkr=9GjzT(V9n3Ten}l0o`g$Y@N-D6>z8;?+f*i6IEJ9g_ zl1VN_qC1Fwlq(E!^XHwKJ1ze+1?^IA$?svBJ>)Ob0e{=zfRN|3+0PR)zfJN?myv!X)iGunr? z7*beE-vPnLtaGfUD(|!~2bJenBrZS5F~YhG*V5WXSU)Kvwz!9Rufwa4ftF6RdxO*Q z+2gczc#VglwJzT03Es!Y`phh+dsTlrhg>E{LPA19LPFxx;{O2`2V4OErV`@-0000< KMNUMnLSTZgE+aGm literal 4733 zcma)AX*`tQ-@b>z@Iyk0&=`e?8ChcNTb67gB4WsBGBcJL%OHd#WDD8%Eh1TxJtUHS zA6pc%?^}rf^m~5K|MP#I&-3DWan8BF=X^ic_d3^g&i&#%H`Lc;V&Gx`0DwtbOWlYv z%KvE)T1t;RCc9DwdS@+j0st_7`qMyhBFq;7fQ40CUB%?#lhu19Pm??Gr>Wl7cXG>K z2DP@cLQrQ_GQ_cQHdId#uz?SKIuc{Z_kku<5Xlf2R61EmSV-Ypr-P$MZEu7ce^`*o z6>z{CVd3{eXU|gIg!1v-%3Z4W=sDgj9Ob%P^t)^4xZ!QX{?c4Mp=UkKyXR<DxHH)!KX^BKxQ()cm&&h*AZ=DgF-Q z8}mrJ(c*=3(zx8SrmxK_?m}v9O$?2~Zwn=y>ieGfuD0}wNn0XYDj?PXTek_VfX^>A z9#i%^gVR%@8sz5@O4!FPv*|aw42-hT>v1?dcg9pI-YGhBDl15{=g1O!uivaX&nqn^ z3HL6Q=wq@r@bz3pZdGM|bk$TwrCvzJ!3DXaB|R`>t9vq{g@?<`gl=}RTq^o0o&L9m z(uK~|gVuQRzFSzr@4c9&akaex*E$R0(bW8WK5MdWLn|bbv{c&e{r(jf@=D_~frp^0 zJmdO@T^iZr!NhTT9Q;zt%-PQb()3azFt2FP(?WS!;T*Nm!8P>{`Y_w-qPQ zyb3cmGa7g4cAD*35_xt;Yc`cmzjtXr5;YKQ0kzDq4hk}6rKc)fYHBNDtUGa&LQT5x zrgDq9t}m#{?oU2oUgm%c)*TM*@CG~Spi4ZQls7fOUN}mfXTA+v^1tm(iP_p=@leYh zSLCHhjE#E4KIg#fw>6pdw)|~B0lGbWm~7GhK*CSv8Ej=lpEl}3qK#&X8cdh1RQm}+ z&+v^}Z#!5na&akxD?P#meiBD8TZ>bO8QUoNMIGJ`a0^rrwRaM1a{=YCmZVw7a(aiZ zBNM>?e7*Ob1b*zv-hkJ{THBF}>YJBkKmD|pneW(QU1hkF{0$tyy1`!6@JwZU57HbS1FEQ`T)GJ$)O(io8D-Ti8D%UF>KAw1!L25 zZ~bnz&n8Oo^LUbPQca^}orfB)G*VCYD=|&i;C$VedOTw{u>zL9Xqq zpI<0*eR6R3^V{uW^1)q49fo%;BFluSp?5F6Ebc{uk;p4v8nN+P!CVW9SsN4&a}jLGdtvFLEJN=_Z*iDXiDYmh81srxL$d?IfraMR&wXjE^X_3 zH-?nrtpUGv`KN-XyEi8W6_~*`{i|0K5M?3_3g$1iLM=>&z%~itLiy?mEwssMY9>KE zxbdJ&y;$lb_g@*_KB_l7?;lVt*9k^RysL6?=(#XHGf;^E+(k}F^cfm)qf*iJ(;pEuSk;%nlGVukKSKid&nlBxkq|1?gn z!3G<2Ba0D3feBDhbWoyV$)z{C7GB=mF2j-HN$cNR$zmZt=u@pn6NRe(F2o4rboD{y zUI?T#?x1Oank?$hrE4E=tABt}g@p8Ny+cziY%$xvEXI0BjyeY-tF^Tq2U8+jrz-g! zrT8WQVO=y65C{QTMb8|_~I=9Dt zVL$n?G|=v~Wf*qNxj;pcvIEcS7#GSbIl=^R6W3p-9N5{x?gV@y5CyC?=@Mdl_ zUaJCL`wfw}ttfx{e&3R8=D*qi$xqHQ9V>XP3T_aS0mqHy*!vYUVFDW=ul(}qf zrN7rqvZ`;=)Hm6jv5Q+W)P9OIF=}$#4KCcjxO7xrc|7+{$M-Qo+k8Temv?5bjtY2u ziaRopbGR75d*){`@Athzc;i*2uFl=_F6WXWo7%6&ZBOt%t$i7uli~KT zZzo-($;OP`PpY)~T%`v+*G#Ia72ZC@Mkbn1ya}Jkx&EaxN~%jF;9_c1P7Q;@oVsUAPg+g?jEMsR!JrlJ*#Kkm=xwKT;1 zh?}cH4NGj3&?Tt(vFXh^e8Hhze&N>}p0|9m(d-8C zcyC2|`rIH(YNwqWo`2<7=m3L=p}lq+;~mzhqVT$3(h-Y8F?3NIlha4FGCa>WQG?Z~ z1Dt3!n2nH_!g7&<+xw|e=S~Nc)D3UC+@SjBT|cJ8N>_G)%#qIyPQI4_hn`bQtkp!- z7jm_E(12@ISQbaHFtFn4-FEiCoOSCwD{BWt*R^{4l|fU8(O_|0F&9@qJl# z%}0mX_bUULK_HrZ8S3fsFA6ZF;~y98^e+hkw6#^w#%<(vKFYIR=j&P}w}t<@$FV<2 z3$U3E^&N7~yx^VE6Yv@T;r(fkBUlJ%=?mOq@ML6)-~}u(ZefZUr%MH z`<4Nt0>s8{WBbMdzkfJp>O^NVm5~q=^&XE+vN6qLu@IK{Sv`^VGi)S0snYkROzp(4 zEKfbtWsc3YyQV4^e|~;_61ma~<9V{5i@sE(I|-JMgf-a|eL!>Rux0Vn%W5VbI$bBo zYl8a(!6(59BFeLL<$)@j3=7R40n-WNm(oog;N)Nx8x?=aSPTN1gHB&`^^OT039qU3 z)Qjg*I29Rt|G^=UpYlt+_|wAiyOy_2>fnl9Tqa$tZF0JblscB<6ZW+Xm6}Q-0}EI5 z0w2^{nI+@RcX{(Wp336v$u_3`C@ui39HY#RNX!nAlKY4tT0PpUwZ!)sWbD4@5_ z_HmM&i7wyQUMIZHqa1^X3iA2`;}N}F?5Zudq;#di{>lXvzNcDX#65cvOUvtp(~saL zne(?A!ltFZFjARcJg=Ssj~LmYJL*`ltSpIsebH!)m!kSn)wQKlozI<9 zd#kY5gmGuF;)0yJ^eAC!@HNi0y*lfI<6br;YjZun#B*+wnUg}z(VMA(_yTiPf=pcI zvTiB)ic4ir#*HxjnXlVOTd9B=ugSm?nfuG@=SR6#9T=ZJc{j8`Qj;M+aIQfRGs)Rx zNGGsyP~HEV>Z4osgiSMN`CecA)5%!J!hBJX$5msxYq_8K41P4z97fZUDw7-~dY^OR zQeG``(L&=e{{BY;u3H=&^$8>~g(`pdx|CnpxACQ}e0-|=5wOhGK-BlpkgKhTxJsU} ztV?d>{!QEDH|+G}G|noJe=qmQ+<;9pR)bw#GU(dc{FSek4R>w%@efssRZ6i>SFBvS zfp#b#$WRy%sz0ewD{4|Zv^_t|Yf6e=h2&Y+j4qnK$7z%N_?|y4%*=cXMvSAZRzLu5 zOrUClyP;Qcv_w}8Q~**#fQHvtRsilM$d6F$IFV)Z3Z}Vd8VHaBfW!{Sryx(PCvvgI zYVsf{%6Z~S&;m3k{#woo*IY&JidC(&m^dFcGm5L%Uzhw45zE3ABd4L{GVM|R-Hso7 zRkfo;wS7(X(Q3%2M#({wZ2r0*b2}YHR#lD$W|d3#{{o?yZ`1e`I^B)GKWrkQ7ZYzz zP~saHR8e0bWDnj?lQuLHwbcE<6tzr{(UIcwip z+)Y54-AEi;N!|gIvCT&1ARve(3B^hk8b^EH?2x#4^CkOb)w0+`RD$VSD;!%W1d?Dg zxnw2>nA|z;o#pT?r$4hYpa)W3&q*-Qj~`m#BCjSfwjM4O>@H)4mpz!1SS(W1*hWG* zpP+<7K3&*y(+fX@05u+q8~axWs6HyR@UsKD9Xez0Nc;PN2C1*LL3}Jmzqq;Qblw38 zT2a56(#ps~ia*ZNs#^xV=l9n79#)gN0xo6A0i3l&=^T~z^jEXbL~7$s$t^`X3sXA> zjXfnOV{xL+Nqj}hYD!vd>NDLhe#?gjciSzTKaj^NHL7^NZKkmfDFza)o<&I$R}$L& zEE)xC>X6Q1nKCrf7ca0o2sh9p&PlY3Ee?lI1jGSmR5DlSHCeNG(to?EK61k>p2_J1 zu+nlcp6R19q?BBlsLlZgKO0;oR<(nW~%7ntlEI!EJn+eCLUG3mP}2YVZ8^h^3x znrZMyEZ44oBh@qk=ztLKYPF33z5NeP`8JgvYmSTPB<~t z11DR1F;AQ`5ow4mhajIwjICqixQ9WZzW!S&ikK`7>df}oj{9zxGq&Hf=q%gfc? z*h}BU&dbqG_JN?1B7?jqk^+FUC!(O9I4quk^i&Z18y89W{xc01g#MjEbW{-3#-SYS zkw^_s4BEuW2@4fRh>0TvDIl&7&`2ZooBu#iA_c*RM4~ei4)^fz5c7}_b8>Zni_6N& z!V%Zu*RP9GGDHa^JQ3w7iYM?>5E!6;W2oB`>|8O*PjM5ENv9{sZm& zAJBNhKMGK^fKwLW;$jkT3KkS9Pl=E!Zp4R9u8QjRuJ(>%y7m~N-9!05Y5%=IDGC2m z<3DQt|9Ma}`}Y?A2MVJi`)rS=nCnV$(HfZ!|8oSiHT2a>RBZzO1vzB8{r~^~ diff --git a/assets/images/support_2.png b/assets/images/support_2.png index 89045ba20678595c929524d8689f02d203352922..8bac085d1cb1f66ec3ddc9daf322a6a27ba0d1e2 100644 GIT binary patch literal 6291 zcmV;E7;NW>P)IItFLrOI3hQJ$dQE01PE71$o=jAy?nGy+nof{k#wit@Ap#as_N>h?_ckI z?|bik?^ATrNhh6j(n%+sLaFbhRhJvB{+o<8|KCkkx6NSj%rICz&l+vsDF(auK9k*- zVl?}Iq39Gp)N^w7l?J=#E`ax*$>!N@w0de(M?j=xtUiZT=W2m-gvsVT4#0i*e?qdu z^-D#k_DS7r%YS6D`ZA0*UxDh#5-Acu01S3VWdJPX3qYTb;-y%C?USm_JHlkqlGKUb zs}!9sEU6al4wKEd1t0-bKLK@KY_K%4k-&ClGuYVTvqu2%5~J0diDMI0yY~-@PTw7+ zrY`!P(dK@}WY4OH)HVxbvRM<|$GJq;v6-S9xJSCi!1i`{MIWb^&*_PCy2*2Bn`{Bd zQo;dt6(rsTpa-cVW?!o4)OMt*-ir-Z_uH&mZPC>*0AsLv0?fM~42VguZxU~Qyh|+0 zIV9HaC==WEo)E<)72?3*N|9f9Qf%CHT&!C6m3Zf~J>sEvwupg`S z6nQ3_doaCZicYa7lU17zaH9g7G~UG$`1gmh#S0(o6l?R##Hor}5s4Zags8`#NK{o* zFShPEA!dEDTcl0TC8=jE0Ik758|{A0U~_j>bc!9R4sS9fs3wBs1_2oL^jh)h>Vx8B zMNJT_^2%DVq2QR9pIIcHoxek5yt+}0er6p3$2=0>alE`iJUM5(n7w?jShM}8I9~qs z_p1SRV9OCP;kAvd8SoNuUyXaIKtKI}F3qu_P5|-~G;Fw`#A@`OM^-biwRI$sESU_{ z6Yp;m{T~VhRW_Jj1iTQ$T9CVwI++UQ6%Bs2`*txjoxaZeH zOJoX+twlx~xe1gzT7ewO-JmdazNss&L^ZN2zB=b*Un*>wbz z>_fHlwy=|>(bXM1`0*UEP%9QEPS*r~WA3UV(F;aeQo4&&HXCB#Jfq$F5{77hqx)5K zIuC#?D?2wfz9-n%_M7Bq!$=ZKEP!R{!f`q;2F&VTYKd&|8MK7#w%ITnG%bf;1b_>x zJF9W1g{{fzIZBQc?0>Gu_*K^^Iz=PV3B!|HRhxgI1ec{M_0iR0XYr|EAN$;b9VDrd zfQ=>44CdrFH;F?>&j_N!$E!s8%shSp!W}6o6LRstb|@)h5r+T5e!=T3Fkh`M2(#8ej{Z=p!CeeZ4e`?(=;?1FN#1lhH|L=gEm9cJJS zmY@`SQm1XdMA0e4&un$ORcAn88#Jn{|MLZpagUy;;$cho2$FjNC7HF!j^dL7#2z$h zbn!SD9t=#OJe-hV1PS9r(-Dc;ywm*Pf(cj%E2Uo(4`X8dEjAX=KWpFu^#vFGaL#RYIS zcp8CjV?e{BQn$m{gC+@eqVWC~KinyZK6LL3N-;cL^}63$62gF=a^EGTSGBpt?t|q# z-bB9}bUDsmwpS2sMO5hiH#UYueqz%v8!U3%D*=`6OZ?U*@lnM6gNh`w24Ybr)kr~MKm-bkC0S*2n=ixL^Bue3<8LTW7lLTp;6(o z%;zW)+=n4PXEuVk;U)8DNF!eh=i+zxd}awI_o+i5p;O@0c<=MQH0I^s45x+eV*XCJ zy+HKpk}v5P0L~W34V$ZUbW+XUE6@-{jVU*7mLBhhQkE73=YxTT1+F3yg*b|oyX~lW z`_tVlZ9|{PWhnz-tj=%A@Un{&``BTEs1i9TxEpv2p+QI|$BPdhspODhGsKZ5^UOQ~ zHZ)|(!?`^N%cY9O^H|)4lsBd*9XcCw^W~uc8z5;9zO|X82#v>e@v$u+?s!uS`~^H{hY60||R7>!6)sAmnaWnz|SR?~LRo?aR5pLaN5DcWMN z`DU2x-ohScj~MgZy01fk`A50YU<`5ay*(2QF_u<;PpF$0Q0!By;Bx^(a<)ent2E%30=-McG}1|F}9vf zw^*|iJL|*XTzMTpEyF`648W%NL<-S$o1MO>or@zkt2cMJFwbA04m*~=)DuW6M z18{Xs9lJ1Z9^Ev+oEd8ORa3HF;!3bZn}Oj&LIGS=U57laW&vjASrb-masmhSJ+nGA z!0dC^7ACHWwz=Qv$N+P?t<~d7n1|dg!Tr#SE+aLSG9ia%>oJ(=GecdK2??;2UhQ*A za!?YT`q`HMK;`N;-ih`Ra)LF9w$558Rr)W;pb3nKt5U6mYJLW09@J6J}C zc8~0rI7{O3+1ofhr<g!*01x#~04^pH0Ff@G_oE$VzUW_zLi zr4>BZh4+e$DlUp@_m!#U%>Pn!YB?FlyZ&Ty_)B|Qd{ibOW*}qu#TGhpuE8Z#j)8s1 z^D+Nv-l`(n$5UKF>3Dgy@NFy=PtVI2lQIA4jgNM*rw!IJ0er?}cHgP!^lhwaw-F6k zMj+pgr1I=7t@Cc7PbUwPtZykfWR{_vnZPp~m*pPfLZukrGN>~#EoB`hscpur#(QV1 z5k1UYC#%|M*3uN6S`G3cgDp!S}q@F!iW|mxd-dsZzomx+ZbnTC5(9I_2YL+_8Cg7Qa=R*LA3z{J)0IB_2g1d%G zFt${RxIW}1I&z|l)iW1cu?GaYjzdTwyiL(5b_5B&8sp5ll=9O(-NUN(-Ou-kvXj;4 zk)&RWnv;d%k$1O%w{pY>6V2IZn)UH+>W8_nl@iOd_7{2fiT)hn(s=y`)$aEh&92`o zI0&kDPcj;DP}bu zF6x}R=n~q7Pk}MnJx?KF;DFH)sO~n--O$_VpJxW%3b!L&6I!DNL4#wovP&ObYGYf*|Bu5~U2d>s-P~(JjuQ@# zE(~-S^|l0V#LT^CunNmAthZ3v!{VzYU{V}DF?d=o`rOUpmBnAuq%f{ho4LM(nEzkm z+b`b4a0jyhF(-K~V$eR!2Gm57#aoMOoiSOx`=BY_Lb$JMr2FfQ(u;JD6z)`XIuBs6 z$Q3PcmE!b&j<8!f$5rkA67+qR&Z*ujln6z*Rr9iIp+3`)2J`{Gvlr_0G>oA$JuLp3 z?&H06h|ku;zul0Ou5n@kxS{)aVozO?*;i|@1*(jw=1!ohR^Mm6Y`*k9Q?h>Jwh<&YJf&Xrosxcd$Da{#2y6n)to z1|8>|QDIGE=6ugCV?X;LsmPWreMkK^No5~^*xRA~GgZK8wj@7}6KKYaoSTeJ|MU3m zBk+-jFTzDYb4n~;&UwahnK$UtEQ$KnWLoUX?k|uFR5f3;j4Q3vx!y`h(UN3qAQjn) z*OG)$!?sd_IuWRJElSd-!sOuhA>qgA{s3?j_vSgfaK|2lRlA#QhK@|XMNk~7&W>ek zDgacIphgEez$%iKxkKjS8$Ho?8|{!F)ZZe>qu=N<_O0&|Xspsh8T&~335cZBMVE2k zKf(VC7;OD;t>;ifWHX9;H<%njU|Ym^7WPl$yOZemJ1n>W;V}Y7HeRb(R*2^MT0eSJXBZ%gBw{I>#4l3$r?kpMdBD<8}}To8^JNWTrL}T z6NyeKwoDT&%~Kd^L^e`jMWagVmyos`Ow!7Z2c~<4QB^=dM_twyktfxx-O5H>qEcV1 zB?Mr%MjHm9&ulQ5-5dH&%-ZtEyW1+>{CIawJn*#H`IY^qWNio7>j7p%G{cKBR1$ab zELg3{R_!H3W1?Eb1}?AU$z8Vgi1_lrDbBiBli$7>%ieKeQul^3RLAw@Vrluqu;?Q` zBc7d}@y&!()h6?bzwpA2Hg8nJ2w9YcY14D^Ut79|3wz3Hv+bI3kG#~*%Zqo5QO~R? zL_xG7JWJG<7}h$L!_y22XjpmvPjzHBY5BsvSlG+q&No2`!cY zNA<*Ey9^Vu635eJ7{9VBT#HqBgPR2y6QuKHd53snzJI*L`;1Kk3}z{=g}sq5Qrro^ z&XRp^LIZ4U9$;qDWU#vDCPYwzDy(9xcn}Y*0~~TLtyGF&$Mp#Su=gI-$-^QY;1K3| zoi^9-1W0*xy$AKI0~|q3hgY5zg9iOt3>;_$zb*z0$i%j&-TR#J&|3kR4se9v2rTY` z1%szu1t8PG*#K39bNikOYH|Mx85E~rWa$7$7!%L9Z&r2PfI+1pfSv--b0EnUaBneu z_|Mz9-#N>qHiD%C9Ku7=mx`@vwxE;()YW3};K5?-*dNB(nuOX=gyD36L)biSzBn;x zI0Jih&`9y|h{xYn;+1IazQzLBo@SXO-C)y-;vQg|yBNO=K%X7@vSY75xc&ye9ULFzZal^1saORckq(sk)s zLZ5BwxH&^>1`Iw<;OjiDAPo7b8Yossr4{08=JsnDb(k1h-HO8S<0RWBA+!`-j7VzD1bLS2P3QH8}6z z$r%#qIIiOWd*%N((zy{CzoP>jBY+{TmjK*iut4Ju2;2V&ICQ8~1pZGY3gMiM^4z>A z051YREzdmx7Lgd|+@S*;BY@GEHrc3~gbjjY%KJXQj;L}`{7qz!!j(4p6b?OzElU@43a@EhQFo4EEIzIIyL zO*+6a0vHXtKuXQ%7J&N)oLh%&R@nZVWRUanB5MVT@ zU-rMo769y*>bz%|YF!LIEHW}Kiv}p>j;1znXp=|)3A8nXK zYFRe^T<~k5Oi`hh0*q_hNnJ-d_gVQ84c7r~8-T|}b9B)DXpFw~s+Izb|3AdOnUP$R zIJO^LNqxNza9aQjj}#A2&_}jaoE8$Cffph<-U^N8sm}NVSmiutZFg9(VU1MN{S}>p z>n3@4feR{X5({p=fJ&>`1g4h~FXuWctQOiwN;SRVLPv4P;%*lJqejOFq}ob6U&DA*;h8l$9u%WIm@+ZMGj!AYX!<)<$Z&KR81=JYEIQ124np z9Evc3OUTj&zpS(m5}h!adNjaYWrRdv2Ef`@F|r&Z6?|#jr|oU^^>L4=zmcih-Q!7m zBLFZgn6UxBaT@^Kt;q}y$vAIX`zv!eZP6v2Q(MQVT01II@;y(8vuWd6dwse`MN$*N z_VuAjcNYX?V0^sk4 zC=|$jp#-@tBpKK3e)cya@SSm8_Wx{AQK}M4C5EXId(TnXS0+0&vn+fP%3Qt>$Lv(J zl+LB^(04nhZoh=f6SoZjgF--F9?L#ij2`_{%B-vv)nQNbO3_n&AT z!e0Y)`mGpCa%o7aY+7rEP%_7BB>0E~vy}SVaCOZ&VDUs{ zsAvG31@_ZWsQIF$d~Sfg%h%w~HcthPZ8X}wPg5=GnD@!j@CuHTG2Cgf$4iDA0oqU+ z4ut@@lp;qF`(z{m#@oE7t{8(JNtw8=62R01$Gpgg*OtY)seWYdieM zRAj-@nj^+{RsRYl)ZudV`M5%WeBik;YL(A$ei{Q zZ`3Cv@UN)#JPzvhU8vk_T89DKGG55TnROac3Gi1}su|g5Qk>aUXITZ)K01cJ7@So> z-vPkK~_e6cZ+X^P6wY%$h&ukNfUB_nx))e*3(2*I75l=%Ef10}lfL0ARYWt7%N0 zmH!Sp8uC-=jqDSgl`TzjTU4I8KRh0QM0C17{zNWf~Tli3B;8W8nTyGmZ&j+E+ zepi~+sDllMrl#&@{Zy3ymaCqM`WY=wtN4~la;-A17H3kdR5PuLnZ)n0S6AM+em(r| z+Zz!M>Y9uZeW0Fa$tz1fM?ql=RGl~ORTD#0@wq;`D#6FOEk{f6st}vuM=kubVyW-$ zeE!g|N!r{Q%&1=Qg+|L$}+X{>2S+)4dBwk?v$N%;y_!QRh2pO4AW12h`g?1<&~oO=F3j8&>FI{?Oe zxKMhRv-lf)jSH5fM5;1Joz8XI0!P+cnua_Vf^=C@Oy?=znI4s^==?}*;k285i3}JC z*FzACiKheBd2mAiU@9q&aKM|VP~b_r#F3^La%I+i*qqqr%h=o1qa|sA7wVx|%(m6} zM6hA!f9h#t-GXs}`la!v$z9g7_|*@becu+_=~$llRW{yd$W)wN7e1pofpEvx`Fm_~ zR~&D>eOVQvh#z2U2$@$%AfB9u7}a8sp60s;a<>&iM8DqQUv%O!K)Bpd%}CE=O+Hez z=W~3xJlP%{I{m`e@P|5 z$v}N)cq`*X!)h}!a&9jvV#wysNr+{0L5f|OdXQYw)oa7ot~$`&egW#$jYPj3d}Y&8 zY07IZ47$|j7Uh>|C3c69(llbw(6JkA@G3N@&+auVH^E+{h+;uL1lp2ybjc+{UVh|e z_G`~ON&VZYpbLM*{i{5PlPcmr(AHJS!uA$|gpqln7VGX&<*|1I5R7uSc{k>*oNIZk ztEq^9nX+lnJF4%mKn1IkX7FEIeCsoXbMUAX@HH2mfz0dF+&~k)CG+KGR~nl?`jY4S zF9RROOMlc833*}1#(l*bKecW+o40ZB{LUe*xvv`tSy?$liI_yM{WCU z7il#Glp4nV@qk~uGoV5h?f?83&B`5}%RzB3Ta+pxACsaZHqRW~{IMl7iRH{Dg}3NI zxyAT876Og`?R0K3w9L1eqv&CozF@gI2cK&UlG{e`*6{XnqAg|?>KhjJ1M-UH6>mFqj!1CoUkIWYNZp51K$)btFXag`vB~ECEPE=Yw zs!eiMphGC?&bVZ^+~{G?dXcZ&uz4`^M<0L1sKN5m(%LQ>CVDR*g?iL+JviYt)NTAm znnQbc-;B$Jrr!+S(|jp93=e906UvJ1qk8m*=BKH@?hnqNJU`SrGuR^S_3D)Su9h}a zQj7Ov70vksmu>xvlWC?!w4NT0K#?h#j}UDc~tQ33z&1 z_ZmHuEn2QZ9ODpmpW9GAd-yA@@7S_j*4JYAH||h3%S5cC9RFr3h4S9%@do5jU~(cgMF`*x@awJobPS$R*OD;4%^o+=^01nsCBXqqW?LBOqB%P>F~@ zu4x4>0tupD(5`<%ceg5PCi^2S&#%ug-Y}lp*d(>k<0d#?7+(3&%)l;Dnu@PO`fVbc z)*~&pBAL5Vswdfb`>^n-x+}=rr_WcjgA}V6{n_5%xUN(MQ@(p|l?%^OkDr`cUsOA& zpoxv=0Msx5sCxmWnr%~AvUB6HA_1+wtj(Dsp}=Z5bnHZCv*_`RwxcG0x3(EnuEQ(7 z5$hO+t27@kG)5*o5(M>v;zo@;iu6{#yVXDksp)C}L&f9G00r~D&)T!imeIiY#!b(&jG zeMGFaHg^%wS^tJ=iNk80Szei_Et>z_JJ9vTx;7WLhl(+6J@y<~Ta61mY0IrMYw-Uy z-7%4FPca&@Kd{SHYt~XsoB-Cl2kgMrf}EaXo^*RvmprSjJ-Xeq`T?WUVqA`CbU7=C ztu0L3sN`Uvp+uc*Mx9@=K=XfA765QOZeC7NkE=U}_H&<^;7)}`UB~B+j<-kJluf+m z4bwN})IQtNjXqwW#fvBV_iT|yqnPh5_djTq5$d)6uwTw@S{YmWwvy3$#^i9&Hp+K( zIfTMvx%$eU%n+h zIG7Np^*!t%USk;wR9hhF>{jrf41VJRs2xvnpOdC5%WGNIsDbw&j1!ltkK>~9tS-&g zmb+JBt|%_~&3G=9tG&S)haG)>LOg87ot~LbZ^tJ2Tb5fL{pxu|?7U*>d6{`AFt`ia z%}wE~FGku)i&!p_SYG}6vq=-d+tM6ig`!xJu4}B67 zorZdBs62*9@im+TNhdiDyvL$eBo6tu`udx#xsje;r|JBW$uqTi z$hAQQlhAt{{$r~xSwz_B7v{4nUOSNf{rH7XkgHNPn~9+1p0;2>G@Ro>%0BmT!V)!mA0>ENpGr@ zApP(`i-=S0ufUH-0}=fO?sH$7J19mj3xlerA%}vw=i972}g<}%cC;aa(m_=Gb&iG8#%C_^LnA8aSy zR`}wb!S&)P#)F&TO$MRM{U;v5Y96R1mW0(|lWoz`$|JdImO?D_3Aa9Ten2Y=h^?+H zdQ-gTKwDjIj!Ek+GzXJp%9vXTWTkX^s>QdQ=}fzNX7@$f(+A$--xzN>?lrqm^GX8B zs6Qs7GCcC6talPu?9g)QM@3T8hZ{~q4YXZzGcZ?kpwk~@5UoAM~7omvK42XI1R zy=-(UcLQ5}ArjFOAoI0I-)kwUQPPQ%)UhHwXKW^XYxSkk81UQDX#<~wMz2nb2H9(N z{yXK9m}n+YLO>$N#%p2-33>X=;go7b{72VE7A19G#mP1cF@OTWnCp^-Y|>5k*keo|2;om{vg z2(8$qjh{GOa>&XhaJ|ncn~(R*y%-s->*v>9CyobImEkAlJ|K->`CYO`vL!nfuEs(j z^>mY-!QbG-D`r=XOVUN|8Kvq;uf=q7q-I^2B*BtStcxpjKHE9OJ$@`tGujb3a=ghu z@yXLBa-%we*cuIsc7lsl5AQ*KdpX%FoAWVUx_Pfcq^T?Q!5_i(#c{b%JjT?YUAJYz zqUn;OT%Wcqk3~wlz3M9KymY(H5h8A9@}~JE1@z%38a5$4i#oeQCPp2{m*DCps*Rs~ zllFX1A7mZdE9G_h6&3K4)Z=Dy{{(wbP7M~x|8A2)SO+}>0Ey8lH8DAA-k+?BGwD}F>N|_o=E@h{TwRK#{Zj~R~r)7vN;{BA9^`Fc=v=eXNK@-S55OVe&b zL+E-4%ac;Xy@?W?QyvD~sL_Tx)ALtA)LNfgjq=4)!B#arje;iGi>Gg1+>Izlj0_4JH6o7?wi3!Q(XfiaZcfs;a^&mTutD-ZFyF|>xwGcR0~Nu6 za$3>`B`$+4Ei0@2IQM`6PR{idz^?Mxk()LP%WVsv5K`^yXB*e2pUY=!cEJmC>xTdQ zJj9Ki*F^OXOH3HKU9Y~<+rU)MdqbnhvmFfist9-^ts!^u$pxFlzB7P{N%l#w72Y~R zuZ%8XV1S1e+Y0iUrra1Uz&g98Z?=b5hWEGgqgj&~O^js>x7x!MfC+4t7Q*vb^tUrD zKS|1eCT` zy?LXaPm7V%mMOOl^1htRlHVD12l+y>ci8OP(qNki4QjebUPdeud@7(l9-C`U%J%5VJ6<=!|OG-zslq=FOmz1;zsDXgG&iEuL^Q zoZ{jx`ATUS1|k={8iNaG;q2AR^l9u;THi8u;p_R+ACLtUS2eNJSjyAwG65?Z?aWHoObH%Q3wvAQD-vi&1%U2_aB>_Ob!_!7$zF^(jY^CL z3BHv(S~(k|Dw)%&({}&TTc&Mee0d1E{Ia7ogbsAm($YRhsGrS&11)AzttTTRIc;=< zqyEB%fzY@6ett=-964;rysRt>G?p(D8@9Um6K;yZ`gaZdf}aqhGK2aUZ*Wlb=mgB9;or04bx;Bwvw_|uSVun#ZbFV=>c5TZq7&R?bwehi@ z8bi;`0tgQtOs8{&-Q=r+)o@rG8xZUj_J2+?vU7xj=yiDVMPBbV?L3zee~@>aBREm~ z6sA#-OYs1?NX^4A1evmY+!0~;{atz@aLlNbNs4Kvg?q&MfhV`466rt9&U~Y{V$H;H=GO0TdBj!`SbWy1e$UL2i1Tq^d0f=%sqR-sojN1g?A3?nf+~|rC@LA8EWEg=-8d!*JS<=I&oQ65PM>kMrX;qZ8JpBV7UghL4 zu01&?N%RzbJ&S2C7}(zr`GsivsUZu#t3`dwHF2xjQG0zv?Od`WTN3uTxDZmDjj|fO zsu8cc*M8~b;yKH{v%gvciv9)wK*0txu|QcE=qo`H9ukhu2qzbb01qVj9+7;ls1|^9 zgu-1=U?&$>n5T;HZd02u80M@ZY%XUYWq{Ohaf9gwdAmS@9-2Ud;7~dByZgkCufNJ1bU(z z1H?Uj1jz^t;D2Fgy7)l7VMr7V;R*hW>F9*;MX3l2Gl2hrM*cTwPoIArAbTN6-jI}* zkdY)~fx*h;NJ-rn<%aN9)pYT8flKJSz)(;(<-ckFwLzwo{LAs*IRF1T$e#WC6aR;f z07oQQhtxkh$Z1L%-Y$+Pg!f;=R5ctu{T!7g|M&9$Ddj)akzFPGEBWs#k%NEN*~ODw YTyJs}$$g6C-(TRq) Date: Sun, 29 Mar 2020 00:25:50 +0000 Subject: [PATCH 04/40] fix(services): enforce minimum replica count of 0 (#3653) * fix(services): enforce minimum replica count of 0 Fixes #3652 Prevents replica count from being set below zero and causing an error. * fix(services): enforce replica count is an integer Prevents users entering decimals in the replica count --- .../datatables/services-datatable/servicesDatatable.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.html b/app/docker/components/datatables/services-datatable/servicesDatatable.html index 7c092c0e4..bcd92884b 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatable.html +++ b/app/docker/components/datatables/services-datatable/servicesDatatable.html @@ -141,7 +141,7 @@ - + From edd86f25061b4df7f795cb3fb1b8aea2056a267d Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sun, 29 Mar 2020 12:54:14 +0300 Subject: [PATCH 05/40] refactor(tags): refactor tag management (#3628) * refactor(tags): replace tags with tag ids * refactor(tags): revert tags to be strings and add tagids * refactor(tags): enable search by tag in home view * refactor(tags): show endpoint tags * refactor(endpoints): expect tagIds on create payload * refactor(endpoints): expect tagIds on update payload * refactor(endpoints): replace TagIds to TagIDs * refactor(endpoints): set endpoint group to get TagIDs * refactor(endpoints): refactor tag-selector to receive tag-ids * refactor(endpoints): show tags in multi-endpoint-selector * chore(tags): revert reformat * refactor(endpoints): remove unneeded bind * refactor(endpoints): change param tags to tagids in endpoint create * refactor(endpoints): remove console.log * refactor(tags): remove deleted tag from endpoint and endpoint group * fix(endpoints): show loading label while loading tags * chore(go): remove obsolete import labels * chore(db): add db version comment * fix(db): add tag service to migrator * refactor(db): add error checks in migrator * style(db): sort props in alphabetical order * style(tags): fix typo Co-Authored-By: Anthony Lapenna * refactor(endpoints): replace tagsMap with tag string representation * refactor(tags): rewrite tag delete to be more readable * refactor(home): rearange code to match former style * refactor(tags): guard against missing model in tag-selector * refactor(tags): rename vars in tag_delete * refactor(tags): allow any authenticated user to fetch tag list * refactor(endpoints): replace controller function with class * refactor(endpoints): replace function with helper * refactor(endpoints): replace controller with class * refactor(tags): revert tags-selector to use 1 way bindings * refactor(endpoints): load empty tag array instead of nil * refactor(endpoints): revert default tag ids * refactor(endpoints): use function in place * refactor(tags): use lodash * style(tags): use parens in arrow functions * fix(tags): remove tag from tag model Co-authored-by: Anthony Lapenna --- api/bolt/datastore.go | 1 + api/bolt/init.go | 2 +- api/bolt/migrator/migrate_dbversion22.go | 57 +++++++++++++++++++ api/bolt/migrator/migrator.go | 12 ++++ api/bolt/tag/tag.go | 13 +++++ api/cmd/portainer/main.go | 6 +- .../endpointgroups/endpointgroup_create.go | 8 +-- .../endpointgroups/endpointgroup_update.go | 6 +- api/http/handler/endpoints/endpoint_create.go | 22 +++---- api/http/handler/endpoints/endpoint_list.go | 39 +++++++++---- api/http/handler/endpoints/endpoint_update.go | 6 +- api/http/handler/endpoints/handler.go | 1 + api/http/handler/tags/handler.go | 6 +- api/http/handler/tags/tag_delete.go | 51 +++++++++++++++++ api/http/server.go | 4 +- api/portainer.go | 13 ++++- .../endpoint-item/endpoint-item-controller.js | 50 ++++++++++++---- .../endpoint-item/endpointItem.html | 8 +-- .../endpoint-item/endpointItem.js | 9 ++- .../endpoint-list/endpoint-list-controller.js | 12 ++-- .../components/endpoint-list/endpoint-list.js | 1 + .../endpoint-list/endpointList.html | 2 + .../forms/group-form/groupForm.html | 3 +- .../forms/schedule-form/schedule-form.js | 1 + .../forms/schedule-form/scheduleForm.html | 4 +- .../multi-endpoint-selector.js | 9 +-- .../multiEndpointSelector.html | 8 +-- .../multiEndpointSelectorController.js | 55 +++++++++--------- .../components/tag-selector/tag-selector.js | 4 +- .../components/tag-selector/tagSelector.html | 9 +-- .../tag-selector/tagSelectorController.js | 39 +++++++------ app/portainer/helpers/tagHelper.js | 9 +++ app/portainer/models/group.js | 8 +-- app/portainer/services/api/endpointService.js | 8 +-- app/portainer/services/fileUpload.js | 8 +-- .../create/createEndpointController.js | 28 ++++----- .../endpoints/create/createendpoint.html | 3 +- .../views/endpoints/edit/endpoint.html | 3 +- .../endpoints/edit/endpointController.js | 6 +- .../groups/create/createGroupController.js | 2 +- .../views/groups/edit/groupController.js | 2 +- app/portainer/views/home/home.html | 1 + app/portainer/views/home/homeController.js | 16 ++++-- .../create/createScheduleController.js | 9 +-- .../schedules/create/createschedule.html | 1 + .../views/schedules/edit/schedule.html | 1 + .../schedules/edit/scheduleController.js | 9 +-- 47 files changed, 404 insertions(+), 171 deletions(-) create mode 100644 api/bolt/migrator/migrate_dbversion22.go create mode 100644 app/portainer/helpers/tagHelper.js diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index a857375f3..c6ce7db61 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -128,6 +128,7 @@ func (store *Store) MigrateData() error { ScheduleService: store.ScheduleService, SettingsService: store.SettingsService, StackService: store.StackService, + TagService: store.TagService, TeamMembershipService: store.TeamMembershipService, TemplateService: store.TemplateService, UserService: store.UserService, diff --git a/api/bolt/init.go b/api/bolt/init.go index bc6e39be5..8e1a0661c 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -16,7 +16,7 @@ func (store *Store) Init() error { Labels: []portainer.Pair{}, UserAccessPolicies: portainer.UserAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{}, - Tags: []string{}, + TagIDs: []portainer.TagID{}, } err = store.EndpointGroupService.CreateEndpointGroup(unassignedGroup) diff --git a/api/bolt/migrator/migrate_dbversion22.go b/api/bolt/migrator/migrate_dbversion22.go new file mode 100644 index 000000000..f2e3c2b6c --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion22.go @@ -0,0 +1,57 @@ +package migrator + +import "github.com/portainer/portainer/api" + +func (m *Migrator) updateEndointsAndEndpointsGroupsToDBVersion23() error { + tags, err := m.tagService.Tags() + if err != nil { + return err + } + + tagsNameMap := make(map[string]portainer.TagID) + for _, tag := range tags { + tagsNameMap[tag.Name] = tag.ID + } + + endpoints, err := m.endpointService.Endpoints() + if err != nil { + return err + } + + for _, endpoint := range endpoints { + endpointTags := make([]portainer.TagID, 0) + for _, tagName := range endpoint.Tags { + tagID, ok := tagsNameMap[tagName] + if ok { + endpointTags = append(endpointTags, tagID) + } + } + endpoint.TagIDs = endpointTags + err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return err + } + } + + endpointGroups, err := m.endpointGroupService.EndpointGroups() + if err != nil { + return err + } + + for _, endpointGroup := range endpointGroups { + endpointGroupTags := make([]portainer.TagID, 0) + for _, tagName := range endpointGroup.Tags { + tagID, ok := tagsNameMap[tagName] + if ok { + endpointGroupTags = append(endpointGroupTags, tagID) + } + } + endpointGroup.TagIDs = endpointGroupTags + err = m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 885cc9ab8..8f4aee085 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -12,6 +12,7 @@ import ( "github.com/portainer/portainer/api/bolt/schedule" "github.com/portainer/portainer/api/bolt/settings" "github.com/portainer/portainer/api/bolt/stack" + "github.com/portainer/portainer/api/bolt/tag" "github.com/portainer/portainer/api/bolt/teammembership" "github.com/portainer/portainer/api/bolt/template" "github.com/portainer/portainer/api/bolt/user" @@ -32,6 +33,7 @@ type ( scheduleService *schedule.Service settingsService *settings.Service stackService *stack.Service + tagService *tag.Service teamMembershipService *teammembership.Service templateService *template.Service userService *user.Service @@ -52,6 +54,7 @@ type ( ScheduleService *schedule.Service SettingsService *settings.Service StackService *stack.Service + TagService *tag.Service TeamMembershipService *teammembership.Service TemplateService *template.Service UserService *user.Service @@ -73,6 +76,7 @@ func NewMigrator(parameters *Parameters) *Migrator { roleService: parameters.RoleService, scheduleService: parameters.ScheduleService, settingsService: parameters.SettingsService, + tagService: parameters.TagService, teamMembershipService: parameters.TeamMembershipService, templateService: parameters.TemplateService, stackService: parameters.StackService, @@ -301,5 +305,13 @@ func (m *Migrator) Migrate() error { } } + // Portainer 1.24.0-dev + if m.currentDBVersion < 23 { + err := m.updateEndointsAndEndpointsGroupsToDBVersion23() + if err != nil { + return err + } + } + return m.versionService.StoreDBVersion(portainer.DBVersion) } diff --git a/api/bolt/tag/tag.go b/api/bolt/tag/tag.go index d54ee6b76..d4a5dc9de 100644 --- a/api/bolt/tag/tag.go +++ b/api/bolt/tag/tag.go @@ -52,6 +52,19 @@ func (service *Service) Tags() ([]portainer.Tag, error) { return tags, err } +// Tag returns a tag by ID. +func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) { + var tag portainer.Tag + identifier := internal.Itob(int(ID)) + + err := internal.GetObject(service.db, BucketName, identifier, &tag) + if err != nil { + return nil, err + } + + return &tag, nil +} + // CreateTag creates a new tag. func (service *Service) CreateTag(tag *portainer.Tag) error { return service.db.Update(func(tx *bolt.Tx) error { diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 236b64d35..46c5bb5ca 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -259,7 +259,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL LogoURL: *flags.Logo, AuthenticationMethod: portainer.AuthenticationInternal, LDAPSettings: portainer.LDAPSettings{ - AnonymousMode: true, + AnonymousMode: true, AutoCreateUsers: true, TLSConfig: portainer.TLSConfiguration{}, SearchSettings: []portainer.LDAPSearchSettings{ @@ -397,7 +397,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain UserAccessPolicies: portainer.UserAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{}, Extensions: []portainer.EndpointExtension{}, - Tags: []string{}, + TagIDs: []portainer.TagID{}, Status: portainer.EndpointStatusUp, Snapshots: []portainer.Snapshot{}, } @@ -440,7 +440,7 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo UserAccessPolicies: portainer.UserAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{}, Extensions: []portainer.EndpointExtension{}, - Tags: []string{}, + TagIDs: []portainer.TagID{}, Status: portainer.EndpointStatusUp, Snapshots: []portainer.Snapshot{}, } diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index 32a617c92..34bdbe754 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -14,15 +14,15 @@ type endpointGroupCreatePayload struct { Name string Description string AssociatedEndpoints []portainer.EndpointID - Tags []string + TagIDs []portainer.TagID } func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { return portainer.Error("Invalid endpoint group name") } - if payload.Tags == nil { - payload.Tags = []string{} + if payload.TagIDs == nil { + payload.TagIDs = []portainer.TagID{} } return nil } @@ -40,7 +40,7 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque Description: payload.Description, UserAccessPolicies: portainer.UserAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{}, - Tags: payload.Tags, + TagIDs: payload.TagIDs, } err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup) diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index 58ea605fc..ef7b4edc4 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -13,7 +13,7 @@ import ( type endpointGroupUpdatePayload struct { Name string Description string - Tags []string + TagIDs []portainer.TagID UserAccessPolicies portainer.UserAccessPolicies TeamAccessPolicies portainer.TeamAccessPolicies } @@ -50,8 +50,8 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque endpointGroup.Description = payload.Description } - if payload.Tags != nil { - endpointGroup.Tags = payload.Tags + if payload.TagIDs != nil { + endpointGroup.TagIDs = payload.TagIDs } updateAuthorizations := false diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 865923149..dfa030927 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -32,7 +32,7 @@ type endpointCreatePayload struct { AzureApplicationID string AzureTenantID string AzureAuthenticationKey string - Tags []string + TagIDs []portainer.TagID } func (payload *endpointCreatePayload) Validate(r *http.Request) error { @@ -54,14 +54,14 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } payload.GroupID = groupID - var tags []string - err = request.RetrieveMultiPartFormJSONValue(r, "Tags", &tags, true) + var tagIDs []portainer.TagID + err = request.RetrieveMultiPartFormJSONValue(r, "TagIds", &tagIDs, true) if err != nil { - return portainer.Error("Invalid Tags parameter") + return portainer.Error("Invalid TagIds parameter") } - payload.Tags = tags - if payload.Tags == nil { - payload.Tags = make([]string, 0) + payload.TagIDs = tagIDs + if payload.TagIDs == nil { + payload.TagIDs = make([]portainer.TagID, 0) } useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true) @@ -187,7 +187,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po TeamAccessPolicies: portainer.TeamAccessPolicies{}, Extensions: []portainer.EndpointExtension{}, AzureCredentials: credentials, - Tags: payload.Tags, + TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, Snapshots: []portainer.Snapshot{}, } @@ -232,7 +232,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, Extensions: []portainer.EndpointExtension{}, - Tags: payload.Tags, + TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, Snapshots: []portainer.Snapshot{}, EdgeKey: edgeKey, @@ -278,7 +278,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) UserAccessPolicies: portainer.UserAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{}, Extensions: []portainer.EndpointExtension{}, - Tags: payload.Tags, + TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, Snapshots: []portainer.Snapshot{}, } @@ -322,7 +322,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) UserAccessPolicies: portainer.UserAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{}, Extensions: []portainer.EndpointExtension{}, - Tags: payload.Tags, + TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, Snapshots: []portainer.Snapshot{}, } diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index 403a3f668..1d98296bf 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api" "github.com/portainer/libhttp/request" @@ -52,7 +52,15 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht } if search != "" { - filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, search) + tags, err := handler.TagsService.Tags() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err} + } + tagsMap := make(map[portainer.TagID]string) + for _, tag := range tags { + tagsMap[tag.ID] = tag.Name + } + filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, tagsMap, search) } if endpointType != 0 { @@ -102,17 +110,17 @@ func filterEndpointsByGroupID(endpoints []portainer.Endpoint, endpointGroupID po return filteredEndpoints } -func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) []portainer.Endpoint { +func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint { filteredEndpoints := make([]portainer.Endpoint, 0) for _, endpoint := range endpoints { - - if endpointMatchSearchCriteria(&endpoint, searchCriteria) { + endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs) + if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) { filteredEndpoints = append(filteredEndpoints, endpoint) continue } - if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, searchCriteria) { + if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) { filteredEndpoints = append(filteredEndpoints, endpoint) } } @@ -120,7 +128,7 @@ func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGro return filteredEndpoints } -func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria string) bool { +func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool { if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) { return true } @@ -134,8 +142,7 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria st } else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" { return true } - - for _, tag := range endpoint.Tags { + for _, tag := range tags { if strings.Contains(strings.ToLower(tag), searchCriteria) { return true } @@ -144,14 +151,14 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria st return false } -func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) bool { +func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool { for _, group := range endpointGroups { if group.ID == endpoint.GroupID { if strings.Contains(strings.ToLower(group.Name), searchCriteria) { return true } - - for _, tag := range group.Tags { + tags := convertTagIDsToTags(tagsMap, group.TagIDs) + for _, tag := range tags { if strings.Contains(strings.ToLower(tag), searchCriteria) { return true } @@ -172,3 +179,11 @@ func filterEndpointsByType(endpoints []portainer.Endpoint, endpointType portaine } return filteredEndpoints } + +func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string { + tags := make([]string, 0) + for _, tagID := range tagIDs { + tags = append(tags, tagsMap[tagID]) + } + return tags +} diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index dd87c22f2..7c02f5f67 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -24,7 +24,7 @@ type endpointUpdatePayload struct { AzureApplicationID *string AzureTenantID *string AzureAuthenticationKey *string - Tags []string + TagIDs []portainer.TagID UserAccessPolicies portainer.UserAccessPolicies TeamAccessPolicies portainer.TeamAccessPolicies } @@ -73,8 +73,8 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * endpoint.GroupID = portainer.EndpointGroupID(*payload.GroupID) } - if payload.Tags != nil { - endpoint.Tags = payload.Tags + if payload.TagIDs != nil { + endpoint.TagIDs = payload.TagIDs } updateAuthorizations := false diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index c655a0eef..6281a4a98 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -37,6 +37,7 @@ type Handler struct { JobService portainer.JobService ReverseTunnelService portainer.ReverseTunnelService SettingsService portainer.SettingsService + TagsService portainer.TagService AuthorizationService *portainer.AuthorizationService } diff --git a/api/http/handler/tags/handler.go b/api/http/handler/tags/handler.go index 33cb59c9d..b5dac0274 100644 --- a/api/http/handler/tags/handler.go +++ b/api/http/handler/tags/handler.go @@ -12,7 +12,9 @@ import ( // Handler is the HTTP handler used to handle tag operations. type Handler struct { *mux.Router - TagService portainer.TagService + TagService portainer.TagService + EndpointService portainer.EndpointService + EndpointGroupService portainer.EndpointGroupService } // NewHandler creates a handler to manage tag operations. @@ -23,7 +25,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { h.Handle("/tags", bouncer.AdminAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost) h.Handle("/tags", - bouncer.AdminAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet) + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet) h.Handle("/tags/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete) diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go index 9c9e9d4e3..2467a38fd 100644 --- a/api/http/handler/tags/tag_delete.go +++ b/api/http/handler/tags/tag_delete.go @@ -15,6 +15,39 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid tag identifier route variable", err} } + tagID := portainer.TagID(id) + + endpoints, err := handler.EndpointService.Endpoints() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + } + + for _, endpoint := range endpoints { + tagIdx := findTagIndex(endpoint.TagIDs, tagID) + if tagIdx != -1 { + endpoint.TagIDs = removeElement(endpoint.TagIDs, tagIdx) + err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} + } + } + } + + endpointGroups, err := handler.EndpointGroupService.EndpointGroups() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + } + + for _, endpointGroup := range endpointGroups { + tagIdx := findTagIndex(endpointGroup.TagIDs, tagID) + if tagIdx != -1 { + endpointGroup.TagIDs = removeElement(endpointGroup.TagIDs, tagIdx) + err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint group", err} + } + } + } err = handler.TagService.DeleteTag(portainer.TagID(id)) if err != nil { @@ -23,3 +56,21 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe return response.Empty(w) } + +func findTagIndex(tags []portainer.TagID, searchTagID portainer.TagID) int { + for idx, tagID := range tags { + if searchTagID == tagID { + return idx + } + } + return -1 +} + +func removeElement(arr []portainer.TagID, index int) []portainer.TagID { + if index < 0 { + return arr + } + lastTagIdx := len(arr) - 1 + arr[index] = arr[lastTagIdx] + return arr[:lastTagIdx] +} diff --git a/api/http/server.go b/api/http/server.go index 066bc7bef..eb4777071 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -7,7 +7,7 @@ import ( "github.com/portainer/portainer/api/http/handler/roles" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/handler" "github.com/portainer/portainer/api/http/handler/auth" @@ -222,6 +222,8 @@ func (server *Server) Start() error { var tagHandler = tags.NewHandler(requestBouncer) tagHandler.TagService = server.TagService + tagHandler.EndpointService = server.EndpointService + tagHandler.EndpointGroupService = server.EndpointGroupService var teamHandler = teams.NewHandler(requestBouncer) teamHandler.TeamService = server.TeamService diff --git a/api/portainer.go b/api/portainer.go index 23b149c82..f0339677d 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -260,7 +260,7 @@ type ( TLSConfig TLSConfiguration `json:"TLSConfig"` Extensions []EndpointExtension `json:"Extensions"` AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` - Tags []string `json:"Tags"` + TagIDs []TagID `json:"TagIds"` Status EndpointStatus `json:"Status"` Snapshots []Snapshot `json:"Snapshots"` UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` @@ -277,6 +277,9 @@ type ( // Deprecated in DBVersion == 18 AuthorizedUsers []UserID `json:"AuthorizedUsers"` AuthorizedTeams []TeamID `json:"AuthorizedTeams"` + + // Deprecated in DBVersion == 22 + Tags []string `json:"Tags"` } // Authorization represents an authorization associated to an operation @@ -426,7 +429,7 @@ type ( Description string `json:"Description"` UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` - Tags []string `json:"Tags"` + TagIDs []TagID `json:"TagIds"` // Deprecated fields Labels []Pair `json:"Labels"` @@ -434,6 +437,9 @@ type ( // Deprecated in DBVersion == 18 AuthorizedUsers []UserID `json:"AuthorizedUsers"` AuthorizedTeams []TeamID `json:"AuthorizedTeams"` + + // Deprecated in DBVersion == 22 + Tags []string `json:"Tags"` } // EndpointExtension represents a deprecated form of Portainer extension @@ -775,6 +781,7 @@ type ( // TagService represents a service for managing tag data TagService interface { Tags() ([]Tag, error) + Tag(ID TagID) (*Tag, error) CreateTag(tag *Tag) error DeleteTag(ID TagID) error } @@ -919,7 +926,7 @@ const ( // APIVersion is the version number of the Portainer API APIVersion = "1.24.0-dev" // DBVersion is the version number of the Portainer database - DBVersion = 22 + DBVersion = 23 // AssetsServerURL represents the URL of the Portainer asset server AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved diff --git a/app/portainer/components/endpoint-list/endpoint-item/endpoint-item-controller.js b/app/portainer/components/endpoint-list/endpoint-item/endpoint-item-controller.js index 62433ee25..61be9faf0 100644 --- a/app/portainer/components/endpoint-list/endpoint-item/endpoint-item-controller.js +++ b/app/portainer/components/endpoint-list/endpoint-item/endpoint-item-controller.js @@ -1,12 +1,42 @@ -angular.module('portainer.app').controller('EndpointItemController', [ - function EndpointItemController() { - var ctrl = this; +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerEndpointTagHelper from 'Portainer/helpers/tagHelper'; - ctrl.editEndpoint = editEndpoint; - - function editEndpoint(event) { - event.stopPropagation(); - ctrl.onEdit(ctrl.model.Id); - } +class EndpointItemController { + /* @ngInject */ + constructor() { + this.editEndpoint = this.editEndpoint.bind(this); } -]); + + editEndpoint(event) { + event.stopPropagation(); + this.onEdit(this.model.Id); + } + + joinTags() { + if (!this.tags) { + return 'Loading tags...'; + } + + if (!this.model.TagIds || !this.model.TagIds.length) { + return ''; + } + + const tagNames = PortainerEndpointTagHelper.idsToTagNames(this.tags, this.model.TagIds); + return _.join(tagNames, ',') + } + + $onInit() { + this.endpointTags = this.joinTags(); + } + + $onChanges({ tags, model }) { + if ((!tags && !model) || (!tags.currentValue && !model.currentValue)) { + return; + } + this.endpointTags = this.joinTags(); + } +} + +angular.module('portainer.app').controller('EndpointItemController', EndpointItemController); +export default EndpointItemController; diff --git a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html index 5060848a0..c8d9f4457 100644 --- a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html +++ b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html @@ -85,14 +85,12 @@ - - + No tags - + - - {{ tag }}{{ $last? '' : ', ' }} - + {{ $ctrl.endpointTags }} diff --git a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.js b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.js index ab12b75a6..ec8f2b1fa 100644 --- a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.js +++ b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.js @@ -1,10 +1,15 @@ +import angular from 'angular'; + +import EndpointItemController from './endpoint-item-controller'; + angular.module('portainer.app').component('endpointItem', { templateUrl: './endpointItem.html', bindings: { model: '<', onSelect: '<', onEdit: '<', - isAdmin:'<' + isAdmin: '<', + tags: '<', }, - controller: 'EndpointItemController' + controller: EndpointItemController, }); diff --git a/app/portainer/components/endpoint-list/endpoint-list-controller.js b/app/portainer/components/endpoint-list/endpoint-list-controller.js index 0abf59e8e..ca5fe52d9 100644 --- a/app/portainer/components/endpoint-list/endpoint-list-controller.js +++ b/app/portainer/components/endpoint-list/endpoint-list-controller.js @@ -29,12 +29,12 @@ angular.module('portainer.app').controller('EndpointListController', ['Datatable if (this.hasBackendPagination()) { this.paginationChangedAction(); } else { - this.state.filteredEndpoints = frontEndpointFilter(this.endpoints, filterValue); + this.state.filteredEndpoints = frontEndpointFilter(this.endpoints, this.tags, filterValue); this.state.loading = false; } } - function frontEndpointFilter(endpoints, filterValue) { + function frontEndpointFilter(endpoints, tags, filterValue) { if (!endpoints || !endpoints.length || !filterValue) { return endpoints; } @@ -47,8 +47,12 @@ angular.module('portainer.app').controller('EndpointListController', ['Datatable _.includes(endpoint.Name.toLowerCase(), lowerCaseKeyword) || _.includes(endpoint.GroupName.toLowerCase(), lowerCaseKeyword) || _.includes(endpoint.URL.toLowerCase(), lowerCaseKeyword) || - _.some(endpoint.Tags, function(tag) { - return _.includes(tag.toLowerCase(), lowerCaseKeyword); + _.some(endpoint.TagIds, tagId => { + const tag = tags.find(t => t.Id === tagId); + if (!tag) { + return false; + } + return _.includes(tag.Name.toLowerCase(), lowerCaseKeyword); }) || _.includes(statusString, keyword) ); diff --git a/app/portainer/components/endpoint-list/endpoint-list.js b/app/portainer/components/endpoint-list/endpoint-list.js index 06835c9e0..1bf184749 100644 --- a/app/portainer/components/endpoint-list/endpoint-list.js +++ b/app/portainer/components/endpoint-list/endpoint-list.js @@ -5,6 +5,7 @@ angular.module('portainer.app').component('endpointList', { titleText: '@', titleIcon: '@', endpoints: '<', + tags: '<', tableKey: '@', dashboardAction: '<', snapshotAction: '<', diff --git a/app/portainer/components/endpoint-list/endpointList.html b/app/portainer/components/endpoint-list/endpointList.html index 028ccc122..d6fd453a3 100644 --- a/app/portainer/components/endpoint-list/endpointList.html +++ b/app/portainer/components/endpoint-list/endpointList.html @@ -33,6 +33,7 @@ on-select="$ctrl.dashboardAction" on-edit="$ctrl.editAction" is-admin="$ctrl.isAdmin" + tags="$ctrl.tags" >
Loading... diff --git a/app/portainer/components/forms/group-form/groupForm.html b/app/portainer/components/forms/group-form/groupForm.html index f16d3bd2c..addd43a01 100644 --- a/app/portainer/components/forms/group-form/groupForm.html +++ b/app/portainer/components/forms/group-form/groupForm.html @@ -28,8 +28,9 @@
diff --git a/app/portainer/components/forms/schedule-form/schedule-form.js b/app/portainer/components/forms/schedule-form/schedule-form.js index 1ca8de98b..557331f24 100644 --- a/app/portainer/components/forms/schedule-form/schedule-form.js +++ b/app/portainer/components/forms/schedule-form/schedule-form.js @@ -69,6 +69,7 @@ angular.module('portainer.app').component('scheduleForm', { model: '=', endpoints: '<', groups: '<', + tags: '<', addLabelAction: '<', removeLabelAction: '<', formAction: '<', diff --git a/app/portainer/components/forms/schedule-form/scheduleForm.html b/app/portainer/components/forms/schedule-form/scheduleForm.html index 83772022d..86e8bdc00 100644 --- a/app/portainer/components/forms/schedule-form/scheduleForm.html +++ b/app/portainer/components/forms/schedule-form/scheduleForm.html @@ -265,9 +265,9 @@
diff --git a/app/portainer/components/multi-endpoint-selector/multi-endpoint-selector.js b/app/portainer/components/multi-endpoint-selector/multi-endpoint-selector.js index 63e0683c9..1862307f0 100644 --- a/app/portainer/components/multi-endpoint-selector/multi-endpoint-selector.js +++ b/app/portainer/components/multi-endpoint-selector/multi-endpoint-selector.js @@ -2,8 +2,9 @@ angular.module('portainer.app').component('multiEndpointSelector', { templateUrl: './multiEndpointSelector.html', controller: 'MultiEndpointSelectorController', bindings: { - 'model': '=', - 'endpoints': '<', - 'groups': '<' - } + model: '=', + endpoints: '<', + groups: '<', + tags: '<', + }, }); diff --git a/app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html b/app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html index 5acf2149d..534a3c91c 100644 --- a/app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html +++ b/app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html @@ -2,13 +2,13 @@ {{ $item.Name }} - ({{ $item.Tags | arraytostr }}) - + ({{ $ctrl.tagIdsToTagNames($item.TagIds) | arraytostr }}) +
{{ endpoint.Name }} - ({{ endpoint.Tags | arraytostr }}) - + ({{ $ctrl.tagIdsToTagNames(endpoint.TagIds) | arraytostr }}) +
diff --git a/app/portainer/components/multi-endpoint-selector/multiEndpointSelectorController.js b/app/portainer/components/multi-endpoint-selector/multiEndpointSelectorController.js index 3b67fd6be..4f3885825 100644 --- a/app/portainer/components/multi-endpoint-selector/multiEndpointSelectorController.js +++ b/app/portainer/components/multi-endpoint-selector/multiEndpointSelectorController.js @@ -1,37 +1,40 @@ import _ from 'lodash-es'; +import PortainerEndpointTagHelper from 'Portainer/helpers/tagHelper'; -angular.module('portainer.app') -.controller('MultiEndpointSelectorController', function () { - var ctrl = this; +import angular from 'angular'; - this.sortGroups = function(groups) { +class MultiEndpointSelectorController { + /* @ngInject */ + constructor() { + this.sortGroups = this.sortGroups.bind(this); + this.groupEndpoints = this.groupEndpoints.bind(this); + this.tagIdsToTagNames = this.tagIdsToTagNames.bind(this); + } + + sortGroups(groups) { return _.sortBy(groups, ['name']); - }; + } - this.groupEndpoints = function(endpoint) { - for (var i = 0; i < ctrl.availableGroups.length; i++) { - var group = ctrl.availableGroups[i]; + groupEndpoints(endpoint) { + for (var i = 0; i < this.availableGroups.length; i++) { + var group = this.availableGroups[i]; if (endpoint.GroupId === group.Id) { return group.Name; } } - }; - - this.$onInit = function() { - this.availableGroups = filterEmptyGroups(this.groups, this.endpoints); - }; - - function filterEmptyGroups(groups, endpoints) { - return groups.filter(function f(group) { - for (var i = 0; i < endpoints.length; i++) { - - var endpoint = endpoints[i]; - if (endpoint.GroupId === group.Id) { - return true; - } - } - return false; - }); } -}); + + tagIdsToTagNames(tagIds) { + return PortainerEndpointTagHelper.idsToTagNames(this.tags, tagIds); + } + + $onInit() { + this.availableGroups = _.filter(this.groups, group => + _.some(this.endpoints, endpoint => endpoint.GroupId == group.Id) + ); + } +} + +export default MultiEndpointSelectorController; +angular.module('portainer.app').controller('MultiEndpointSelectorController', MultiEndpointSelectorController); diff --git a/app/portainer/components/tag-selector/tag-selector.js b/app/portainer/components/tag-selector/tag-selector.js index a29a5eeaf..540b16902 100644 --- a/app/portainer/components/tag-selector/tag-selector.js +++ b/app/portainer/components/tag-selector/tag-selector.js @@ -3,6 +3,6 @@ angular.module('portainer.app').component('tagSelector', { controller: 'TagSelectorController', bindings: { tags: '<', - model: '=' - } + model: '=', + }, }); diff --git a/app/portainer/components/tag-selector/tagSelector.html b/app/portainer/components/tag-selector/tagSelector.html index be92e1aa4..d453f1acc 100644 --- a/app/portainer/components/tag-selector/tagSelector.html +++ b/app/portainer/components/tag-selector/tagSelector.html @@ -3,8 +3,8 @@ Selected tags
- - {{ tag }} + + {{ tag.Name }} @@ -20,10 +20,11 @@ type="text" ng-model="$ctrl.state.selectedValue" id="tags" class="form-control" placeholder="Select tags..." - uib-typeahead="tag for tag in $ctrl.tags | filter:$viewValue | limitTo:7" + uib-typeahead="tag.Id as tag.Name for tag in $ctrl.tags | filter: $ctrl.filterSelected | filter:$viewValue | limitTo:7" typeahead-on-select="$ctrl.selectTag($item, $model, $label)" typeahead-no-results="$ctrl.state.noResult" - typeahead-show-hint="true" typeahead-min-length="0" /> + typeahead-show-hint="true" typeahead-min-length="0" + />
diff --git a/app/portainer/components/tag-selector/tagSelectorController.js b/app/portainer/components/tag-selector/tagSelectorController.js index d575f8ab2..3d64485e3 100644 --- a/app/portainer/components/tag-selector/tagSelectorController.js +++ b/app/portainer/components/tag-selector/tagSelectorController.js @@ -1,32 +1,35 @@ import _ from 'lodash-es'; -angular.module('portainer.app') -.controller('TagSelectorController', function () { - - this.$onChanges = function(changes) { - if(angular.isDefined(changes.tags.currentValue)) { - this.tags = _.difference(changes.tags.currentValue, this.model); - } +angular.module('portainer.app').controller('TagSelectorController', function() { + this.$onInit = function() { + this.state.selectedTags = _.map(this.model, (id) => _.find(this.tags, (t) => t.Id === id)); }; this.state = { selectedValue: '', - noResult: false + selectedTags: [], + noResult: false, }; this.selectTag = function($item) { this.state.selectedValue = ''; - this.model.push($item); - this.tags = _.remove(this.tags, function(item) { - return item !== $item; - }); + this.model.push($item.Id); + this.state.selectedTags.push($item); }; - this.removeTag = function(tag) { - var idx = this.model.indexOf(tag); - if (idx > -1) { - this.model.splice(idx, 1); - this.tags.push(tag); - } + this.removeTag = function removeTag(tag) { + _.remove(this.state.selectedTags, { Id: tag.Id }); + _.remove(this.model, (id) => id === tag.Id); }; + + this.filterSelected = filterSelected.bind(this); + + function filterSelected($item) { + if (!this.model) { + return true; + } + return !_.includes(this.model, $item.Id); + } + window._remove = _.remove; }); + diff --git a/app/portainer/helpers/tagHelper.js b/app/portainer/helpers/tagHelper.js new file mode 100644 index 000000000..e07ba6c36 --- /dev/null +++ b/app/portainer/helpers/tagHelper.js @@ -0,0 +1,9 @@ +import _ from 'lodash'; + +export default class PortainerEndpointTagHelper { + static idsToTagNames(tags, ids) { + const filteredTags = _.filter(tags, tag => _.includes(ids, tag.Id)); + const tagNames = _.map(filteredTags, 'Name'); + return tagNames; + } +} diff --git a/app/portainer/models/group.js b/app/portainer/models/group.js index 79b9b3e53..ca806f582 100644 --- a/app/portainer/models/group.js +++ b/app/portainer/models/group.js @@ -1,14 +1,14 @@ export function EndpointGroupDefaultModel() { this.Name = ''; this.Description = ''; - this.Tags = []; + this.TagIds = []; } export function EndpointGroupModel(data) { this.Id = data.Id; this.Name = data.Name; this.Description = data.Description; - this.Tags = data.Tags; + this.TagIds = data.TagIds; this.AuthorizedUsers = data.AuthorizedUsers; this.AuthorizedTeams = data.AuthorizedTeams; this.UserAccessPolicies = data.UserAccessPolicies; @@ -18,7 +18,7 @@ export function EndpointGroupModel(data) { export function EndpointGroupCreateRequest(model, endpoints) { this.Name = model.Name; this.Description = model.Description; - this.Tags = model.Tags; + this.TagIds = model.TagIds; this.AssociatedEndpoints = endpoints; } @@ -26,7 +26,7 @@ export function EndpointGroupUpdateRequest(model, endpoints) { this.id = model.Id; this.Name = model.Name; this.Description = model.Description; - this.Tags = model.Tags; + this.TagIds = model.TagIds; this.AssociatedEndpoints = endpoints; this.UserAccessPolicies = model.UserAccessPolicies; this.TeamAccessPolicies = model.TeamAccessPolicies; diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 7a43ee5b2..6de03711d 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -63,7 +63,7 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { return deferred.promise; }; - service.createRemoteEndpoint = function(name, type, URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { + service.createRemoteEndpoint = function(name, type, URL, PublicURL, groupID, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { var deferred = $q.defer(); var endpointURL = URL; @@ -71,7 +71,7 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { endpointURL = 'tcp://' + URL; } - FileUploadService.createEndpoint(name, type, endpointURL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) + FileUploadService.createEndpoint(name, type, endpointURL, PublicURL, groupID, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) .then(function success(response) { deferred.resolve(response.data); }) @@ -82,10 +82,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { return deferred.promise; }; - service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey, groupId, tags) { + service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey, groupId, tagIds) { var deferred = $q.defer(); - FileUploadService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tags) + FileUploadService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) .then(function success(response) { deferred.resolve(response.data); }) diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index 1f17916bc..07baed73f 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -100,7 +100,7 @@ angular.module('portainer.app') }); }; - service.createEndpoint = function(name, type, URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { + service.createEndpoint = function(name, type, URL, PublicURL, groupID, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { return Upload.upload({ url: 'api/endpoints', data: { @@ -109,7 +109,7 @@ angular.module('portainer.app') URL: URL, PublicURL: PublicURL, GroupID: groupID, - Tags: Upload.json(tags), + TagIds: Upload.json(tagIds), TLS: TLS, TLSSkipVerify: TLSSkipVerify, TLSSkipClientVerify: TLSSkipClientVerify, @@ -121,14 +121,14 @@ angular.module('portainer.app') }); }; - service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey, groupId, tags) { + service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey, groupId, tagIds) { return Upload.upload({ url: 'api/endpoints', data: { Name: name, EndpointType: 3, GroupID: groupId, - Tags: Upload.json(tags), + TagIds: Upload.json(tagIds), AzureApplicationID: applicationId, AzureTenantID: tenantId, AzureAuthenticationKey: authenticationKey diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index 1cdf54409..968aa041b 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -18,7 +18,7 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, AzureApplicationId: '', AzureTenantId: '', AzureAuthenticationKey: '', - Tags: [] + TagIds: [] }; $scope.copyAgentCommand = function() { @@ -40,7 +40,7 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, var URL = $filter('stripprotocol')($scope.formValues.URL); var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL; var groupId = $scope.formValues.GroupId; - var tags = $scope.formValues.Tags; + var tagIds = $scope.formValues.TagIds; var securityData = $scope.formValues.SecurityFormData; var TLS = securityData.TLS; @@ -51,7 +51,7 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert; var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey; - addEndpoint(name, 1, URL, publicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); + addEndpoint(name, 1, URL, publicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); }; $scope.addAgentEndpoint = function() { @@ -59,18 +59,18 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, var URL = $filter('stripprotocol')($scope.formValues.URL); var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL; var groupId = $scope.formValues.GroupId; - var tags = $scope.formValues.Tags; + var tagIds = $scope.formValues.TagIds; - addEndpoint(name, 2, URL, publicURL, groupId, tags, true, true, true, null, null, null); + addEndpoint(name, 2, URL, publicURL, groupId, tagIds, true, true, true, null, null, null); }; $scope.addEdgeAgentEndpoint = function() { var name = $scope.formValues.Name; var groupId = $scope.formValues.GroupId; - var tags = $scope.formValues.Tags; + var tagIds = $scope.formValues.TagIds; var URL = $scope.formValues.URL; - addEndpoint(name, 4, URL, "", groupId, tags, false, false, false, null, null, null); + addEndpoint(name, 4, URL, "", groupId, tagIds, false, false, false, null, null, null); }; $scope.addAzureEndpoint = function() { @@ -79,14 +79,14 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, var tenantId = $scope.formValues.AzureTenantId; var authenticationKey = $scope.formValues.AzureAuthenticationKey; var groupId = $scope.formValues.GroupId; - var tags = $scope.formValues.Tags; + var tagIds = $scope.formValues.TagIds; - createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tags); + createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds); }; - function createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tags) { + function createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) { $scope.state.actionInProgress = true; - EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tags) + EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) .then(function success() { Notifications.success('Endpoint created', name); $state.go('portainer.endpoints', {}, {reload: true}); @@ -99,9 +99,9 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, }); } - function addEndpoint(name, type, URL, PublicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { + function addEndpoint(name, type, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { $scope.state.actionInProgress = true; - EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) + EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) .then(function success(data) { Notifications.success('Endpoint created', name); if (type === 4) { @@ -121,7 +121,7 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, function initView() { $q.all({ groups: GroupService.groups(), - tags: TagService.tagNames() + tags: TagService.tags() }) .then(function success(data) { $scope.groups = data.groups; diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index b930e2518..89308e179 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -259,8 +259,9 @@
diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index 81f840a1c..d33e8bd7a 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -165,8 +165,9 @@
diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index aa023d509..b01a5f1ad 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -41,12 +41,12 @@ function ($q, $scope, $state, $transition$, $filter, clipboard, EndpointService, var TLSMode = securityData.TLSMode; var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only'); var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only'); - + var payload = { Name: endpoint.Name, PublicURL: endpoint.PublicURL, GroupID: endpoint.GroupId, - Tags: endpoint.Tags, + TagIds: endpoint.TagIds, TLS: TLS, TLSSkipVerify: TLSSkipVerify, TLSSkipClientVerify: TLSSkipClientVerify, @@ -96,7 +96,7 @@ function ($q, $scope, $state, $transition$, $filter, clipboard, EndpointService, $q.all({ endpoint: EndpointService.endpoint($transition$.params().id), groups: GroupService.groups(), - tags: TagService.tagNames() + tags: TagService.tags() }) .then(function success(data) { var endpoint = data.endpoint; diff --git a/app/portainer/views/groups/create/createGroupController.js b/app/portainer/views/groups/create/createGroupController.js index 0f1e86ad3..109e70ca1 100644 --- a/app/portainer/views/groups/create/createGroupController.js +++ b/app/portainer/views/groups/create/createGroupController.js @@ -32,7 +32,7 @@ function ($q, $scope, $state, GroupService, EndpointService, TagService, Notific }; function initView() { - TagService.tagNames() + TagService.tags() .then((tags) => { $scope.availableTags = tags; $scope.associatedEndpoints = []; diff --git a/app/portainer/views/groups/edit/groupController.js b/app/portainer/views/groups/edit/groupController.js index ed3967828..d173e40b1 100644 --- a/app/portainer/views/groups/edit/groupController.js +++ b/app/portainer/views/groups/edit/groupController.js @@ -28,7 +28,7 @@ function ($q, $scope, $state, $transition$, GroupService, TagService, Notificati $q.all({ group: GroupService.group(groupId), - tags: TagService.tagNames() + tags: TagService.tags() }) .then(function success(data) { $scope.group = data.group; diff --git a/app/portainer/views/home/home.html b/app/portainer/views/home/home.html index dea3dff6c..043cfa1f0 100644 --- a/app/portainer/views/home/home.html +++ b/app/portainer/views/home/home.html @@ -34,6 +34,7 @@ Date: Mon, 6 Apr 2020 00:06:59 +0300 Subject: [PATCH 06/40] chore(yarn): change start:client to start webpack dev server (#3595) * chore(yarn): change start:client to start webpack dev server * Update package.json Co-authored-by: Anthony Lapenna --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9cc2882da..050de62d3 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "clean": "grunt clean:all", "start": "grunt clean:all && grunt start", "start:server": "grunt clean:server && grunt start:server", - "start:client": "grunt clean:client && grunt start:client", + "dev:client": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.develop.js", "build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer", "clean:all": "grunt clean:all" }, From db8b3d6e5a26aac3a32de1aa27b3a32e7b721ae6 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 8 Apr 2020 10:56:24 +0300 Subject: [PATCH 07/40] create tag from tag selector (#3640) * feat(tags): add button to save tag when doesn't exist * feat(endpoints): allow the creating of tags in endpoint edit * feat(groups): allow user to create tags in create group * feat(groups): allow user to create tags in edit group * feat(endpoint): allow user to create tags from endpoint create * feat(tags): allow the creation of a new tag from dropdown * feat(tag): replace "add" with "create" * feat(tags): show tags input when not tags * feat(tags): hide create message when not allowed * refactor(tags): replace component controller with class * refactor(tags): replace native methods with lodash * refactor(tags): remove unused onChangeTags function * refactor(tags): remove on-change binding * style(tags): remove white space * refactor(endpoint-groups): move controller to separate file * fix(groups): allow admin to create tag in group form * refactor(endpoints): wrap async function with try catch and $async * style(tags): wrap arrow function args with parenthesis * refactor(endpoints): return $async functions * refactor(tags): throw error in the format Notification expects --- .../components/forms/group-form/group-form.js | 92 +----------------- .../forms/group-form/groupForm.html | 4 +- .../forms/group-form/groupFormController.js | 94 +++++++++++++++++++ .../components/tag-selector/tag-selector.js | 2 + .../components/tag-selector/tagSelector.html | 8 +- .../tag-selector/tagSelectorController.js | 89 ++++++++++++------ app/portainer/services/api/tagService.js | 10 +- .../create/createEndpointController.js | 24 ++++- .../endpoints/create/createendpoint.html | 6 +- .../views/endpoints/edit/endpoint.html | 4 +- .../endpoints/edit/endpointController.js | 25 ++++- .../groups/create/createGroupController.js | 19 +++- .../views/groups/create/creategroup.html | 1 + app/portainer/views/groups/edit/group.html | 1 + .../views/groups/edit/groupController.js | 21 ++++- 15 files changed, 253 insertions(+), 147 deletions(-) create mode 100644 app/portainer/components/forms/group-form/groupFormController.js diff --git a/app/portainer/components/forms/group-form/group-form.js b/app/portainer/components/forms/group-form/group-form.js index 10e12f62c..db298082a 100644 --- a/app/portainer/components/forms/group-form/group-form.js +++ b/app/portainer/components/forms/group-form/group-form.js @@ -1,92 +1,5 @@ -import _ from 'lodash-es'; import angular from 'angular'; - -class GroupFormController { - /* @ngInject */ - constructor($q, EndpointService, GroupService, Notifications) { - this.$q = $q; - this.EndpointService = EndpointService; - this.GroupService = GroupService; - this.Notifications = Notifications; - - this.associateEndpoint = this.associateEndpoint.bind(this); - this.dissociateEndpoint = this.dissociateEndpoint.bind(this); - this.getPaginatedEndpointsByGroup = this.getPaginatedEndpointsByGroup.bind(this); - } - - $onInit() { - this.state = { - available: { - limit: '10', - filter: '', - pageNumber: 1, - totalCount: 0 - }, - associated: { - limit: '10', - filter: '', - pageNumber: 1, - totalCount: 0 - } - }; - } - associateEndpoint(endpoint) { - if (this.pageType === 'create' && !_.includes(this.associatedEndpoints, endpoint)) { - this.associatedEndpoints.push(endpoint); - } else if (this.pageType === 'edit') { - this.GroupService.addEndpoint(this.model.Id, endpoint) - .then(() => { - this.Notifications.success('Success', 'Endpoint successfully added to group'); - this.reloadTablesContent(); - }) - .catch((err) => this.Notifications.error('Error', err, 'Unable to add endpoint to group')); - } - } - - dissociateEndpoint(endpoint) { - if (this.pageType === 'create') { - _.remove(this.associatedEndpoints, (item) => item.Id === endpoint.Id); - } else if (this.pageType === 'edit') { - this.GroupService.removeEndpoint(this.model.Id, endpoint.Id) - .then(() => { - this.Notifications.success('Success', 'Endpoint successfully removed from group'); - this.reloadTablesContent(); - }) - .catch((err) => this.Notifications.error('Error', err, 'Unable to remove endpoint from group')); - } - } - - reloadTablesContent() { - this.getPaginatedEndpointsByGroup(this.pageType, 'available'); - this.getPaginatedEndpointsByGroup(this.pageType, 'associated'); - this.GroupService.group(this.model.Id) - .then((data) => { - this.model = data; - }) - } - - getPaginatedEndpointsByGroup(pageType, tableType) { - if (tableType === 'available') { - const context = this.state.available; - const start = (context.pageNumber - 1) * context.limit + 1; - this.EndpointService.endpointsByGroup(start, context.limit, context.filter, 1) - .then((data) => { - this.availableEndpoints = data.value; - this.state.available.totalCount = data.totalCount; - }); - } else if (tableType === 'associated' && pageType === 'edit') { - const groupId = this.model.Id ? this.model.Id : 1; - const context = this.state.associated; - const start = (context.pageNumber - 1) * context.limit + 1; - this.EndpointService.endpointsByGroup(start, context.limit, context.filter, groupId) - .then((data) => { - this.associatedEndpoints = data.value; - this.state.associated.totalCount = data.totalCount; - }); - } - // ignore (associated + create) group as there is no backend pagination for this table - } -} +import GroupFormController from './groupFormController'; angular.module('portainer.app').component('groupForm', { templateUrl: './groupForm.html', @@ -102,6 +15,7 @@ angular.module('portainer.app').component('groupForm', { removeLabelAction: '<', formAction: '<', formActionLabel: '@', - actionInProgress: '<' + actionInProgress: '<', + onCreateTag: '<' } }); diff --git a/app/portainer/components/forms/group-form/groupForm.html b/app/portainer/components/forms/group-form/groupForm.html index addd43a01..5a0526b02 100644 --- a/app/portainer/components/forms/group-form/groupForm.html +++ b/app/portainer/components/forms/group-form/groupForm.html @@ -28,9 +28,11 @@
diff --git a/app/portainer/components/forms/group-form/groupFormController.js b/app/portainer/components/forms/group-form/groupFormController.js new file mode 100644 index 000000000..95db59e60 --- /dev/null +++ b/app/portainer/components/forms/group-form/groupFormController.js @@ -0,0 +1,94 @@ +import _ from 'lodash-es'; +import angular from 'angular'; + +class GroupFormController { + /* @ngInject */ + constructor($q, EndpointService, GroupService, Notifications, Authentication) { + this.$q = $q; + this.EndpointService = EndpointService; + this.GroupService = GroupService; + this.Notifications = Notifications; + this.Authentication = Authentication; + + this.associateEndpoint = this.associateEndpoint.bind(this); + this.dissociateEndpoint = this.dissociateEndpoint.bind(this); + this.getPaginatedEndpointsByGroup = this.getPaginatedEndpointsByGroup.bind(this); + } + + $onInit() { + this.state = { + available: { + limit: '10', + filter: '', + pageNumber: 1, + totalCount: 0 + }, + associated: { + limit: '10', + filter: '', + pageNumber: 1, + totalCount: 0 + }, + allowCreateTag: this.Authentication.isAdmin() + }; + } + associateEndpoint(endpoint) { + if (this.pageType === 'create' && !_.includes(this.associatedEndpoints, endpoint)) { + this.associatedEndpoints.push(endpoint); + } else if (this.pageType === 'edit') { + this.GroupService.addEndpoint(this.model.Id, endpoint) + .then(() => { + this.Notifications.success('Success', 'Endpoint successfully added to group'); + this.reloadTablesContent(); + }) + .catch((err) => this.Notifications.error('Error', err, 'Unable to add endpoint to group')); + } + } + + dissociateEndpoint(endpoint) { + if (this.pageType === 'create') { + _.remove(this.associatedEndpoints, (item) => item.Id === endpoint.Id); + } else if (this.pageType === 'edit') { + this.GroupService.removeEndpoint(this.model.Id, endpoint.Id) + .then(() => { + this.Notifications.success('Success', 'Endpoint successfully removed from group'); + this.reloadTablesContent(); + }) + .catch((err) => this.Notifications.error('Error', err, 'Unable to remove endpoint from group')); + } + } + + reloadTablesContent() { + this.getPaginatedEndpointsByGroup(this.pageType, 'available'); + this.getPaginatedEndpointsByGroup(this.pageType, 'associated'); + this.GroupService.group(this.model.Id) + .then((data) => { + this.model = data; + }) + } + + getPaginatedEndpointsByGroup(pageType, tableType) { + if (tableType === 'available') { + const context = this.state.available; + const start = (context.pageNumber - 1) * context.limit + 1; + this.EndpointService.endpointsByGroup(start, context.limit, context.filter, 1) + .then((data) => { + this.availableEndpoints = data.value; + this.state.available.totalCount = data.totalCount; + }); + } else if (tableType === 'associated' && pageType === 'edit') { + const groupId = this.model.Id ? this.model.Id : 1; + const context = this.state.associated; + const start = (context.pageNumber - 1) * context.limit + 1; + this.EndpointService.endpointsByGroup(start, context.limit, context.filter, groupId) + .then((data) => { + this.associatedEndpoints = data.value; + this.state.associated.totalCount = data.totalCount; + }); + } + // ignore (associated + create) group as there is no backend pagination for this table + } +} + +angular.module('portainer.app').controller('GroupFormController', GroupFormController); +export default GroupFormController; \ No newline at end of file diff --git a/app/portainer/components/tag-selector/tag-selector.js b/app/portainer/components/tag-selector/tag-selector.js index 540b16902..fc05755cf 100644 --- a/app/portainer/components/tag-selector/tag-selector.js +++ b/app/portainer/components/tag-selector/tag-selector.js @@ -4,5 +4,7 @@ angular.module('portainer.app').component('tagSelector', { bindings: { tags: '<', model: '=', + onCreate: '<', + allowCreate: '<' }, }); diff --git a/app/portainer/components/tag-selector/tagSelector.html b/app/portainer/components/tag-selector/tagSelector.html index d453f1acc..144f04624 100644 --- a/app/portainer/components/tag-selector/tagSelector.html +++ b/app/portainer/components/tag-selector/tagSelector.html @@ -15,24 +15,24 @@ -
+
-
+
No tags available.
-
+
No tags matching your filter. diff --git a/app/portainer/components/tag-selector/tagSelectorController.js b/app/portainer/components/tag-selector/tagSelectorController.js index 3d64485e3..40b99d3e5 100644 --- a/app/portainer/components/tag-selector/tagSelectorController.js +++ b/app/portainer/components/tag-selector/tagSelectorController.js @@ -1,35 +1,62 @@ +import angular from 'angular'; import _ from 'lodash-es'; -angular.module('portainer.app').controller('TagSelectorController', function() { - this.$onInit = function() { - this.state.selectedTags = _.map(this.model, (id) => _.find(this.tags, (t) => t.Id === id)); - }; - - this.state = { - selectedValue: '', - selectedTags: [], - noResult: false, - }; - - this.selectTag = function($item) { - this.state.selectedValue = ''; - this.model.push($item.Id); - this.state.selectedTags.push($item); - }; - - this.removeTag = function removeTag(tag) { - _.remove(this.state.selectedTags, { Id: tag.Id }); - _.remove(this.model, (id) => id === tag.Id); - }; - - this.filterSelected = filterSelected.bind(this); - - function filterSelected($item) { - if (!this.model) { - return true; - } - return !_.includes(this.model, $item.Id); +class TagSelectorController { + /* @ngInject */ + constructor() { + this.state = { + selectedValue: '', + selectedTags: [], + noResult: false, + }; } - window._remove = _.remove; -}); + removeTag(tag) { + _.remove(this.model, (id) => tag.Id === id); + _.remove(this.state.selectedTags, { Id: tag.Id }); + } + + selectTag($item) { + this.state.selectedValue = ''; + if ($item.create && this.allowCreate) { + this.onCreate($item.value); + return; + } + this.state.selectedTags.push($item); + this.model.push($item.Id); + } + + filterTags(searchValue) { + let filteredTags = _.filter(this.tags, (tag) => !_.includes(this.model, tag.Id)); + if (!searchValue) { + return filteredTags.slice(0, 7); + } + + const exactTag = _.find(this.tags, (tag) => tag.Name === searchValue); + filteredTags = _.filter(filteredTags, (tag) => _.includes(tag.Name.toLowerCase(), searchValue.toLowerCase())); + if (exactTag || !this.allowCreate) { + return filteredTags.slice(0, 7); + } + + return filteredTags.slice(0, 6).concat({ Name: `Create "${searchValue}"`, create: true, value: searchValue }); + } + + generateSelectedTags(model, tags) { + this.state.selectedTags = _.map(model, (id) => _.find(tags, (t) => t.Id === id)); + } + + $onInit() { + this.generateSelectedTags(this.model, this.tags); + } + + $onChanges({ tags, model }) { + const tagsValue = tags && tags.currentValue ? tags.currentValue : this.tags; + const modelValue = model && model.currentValue ? model.currentValue : this.model; + if (modelValue && tagsValue) { + this.generateSelectedTags(modelValue, tagsValue); + } + } +} + +export default TagSelectorController; +angular.module('portainer.app').controller('TagSelectorController', TagSelectorController); diff --git a/app/portainer/services/api/tagService.js b/app/portainer/services/api/tagService.js index b8c6e3918..d7c952dd2 100644 --- a/app/portainer/services/api/tagService.js +++ b/app/portainer/services/api/tagService.js @@ -35,12 +35,16 @@ angular.module('portainer.app') return deferred.promise; }; - service.createTag = function(name) { + service.createTag = async function(name) { var payload = { Name: name }; - - return Tags.create({}, payload).$promise; + try { + const tag = await Tags.create({}, payload).$promise; + return new TagViewModel(tag); + } catch(err) { + throw { msg: 'Unable to create tag', err }; + } }; service.deleteTag = function(id) { diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index 968aa041b..3bb7a989f 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -1,12 +1,12 @@ import {EndpointSecurityFormData} from '../../../components/endpointSecurity/porEndpointSecurityModel'; -angular.module('portainer.app') -.controller('CreateEndpointController', ['$q', '$scope', '$state', '$filter', 'clipboard', 'EndpointService', 'GroupService', 'TagService', 'Notifications', -function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, TagService, Notifications) { +angular.module('portainer.app').controller('CreateEndpointController', +function CreateEndpointController($async, $q, $scope, $state, $filter, clipboard, EndpointService, GroupService, TagService, Notifications, Authentication) { $scope.state = { EnvironmentType: 'agent', - actionInProgress: false + actionInProgress: false, + allowCreateTag: Authentication.isAdmin() }; $scope.formValues = { @@ -84,6 +84,20 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds); }; + $scope.onCreateTag = function onCreateTag(tagName) { + return $async(onCreateTagAsync, tagName); + } + + async function onCreateTagAsync(tagName) { + try { + const tag = await TagService.createTag(tagName); + $scope.availableTags = $scope.availableTags.concat(tag); + $scope.formValues.TagIds = $scope.formValues.TagIds.concat(tag.Id); + } catch(err) { + Notifications.error('Failue', err, 'Unable to create tag'); + } + } + function createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) { $scope.state.actionInProgress = true; EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) @@ -133,4 +147,4 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, } initView(); -}]); +}); diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index 89308e179..955453464 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -259,10 +259,12 @@
+ allow-create="state.allowCreateTag" + on-create="onCreateTag" + >
diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index d33e8bd7a..827a9aa97 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -165,9 +165,11 @@
diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index b01a5f1ad..451b6955f 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -3,8 +3,8 @@ import uuidv4 from 'uuid/v4'; import {EndpointSecurityFormData} from '../../../components/endpointSecurity/porEndpointSecurityModel'; angular.module('portainer.app') -.controller('EndpointController', ['$q', '$scope', '$state', '$transition$', '$filter', 'clipboard', 'EndpointService', 'GroupService', 'TagService', 'EndpointProvider', 'Notifications', -function ($q, $scope, $state, $transition$, $filter, clipboard, EndpointService, GroupService, TagService, EndpointProvider, Notifications) { +.controller('EndpointController', +function EndpointController($async, $q, $scope, $state, $transition$, $filter, clipboard, EndpointService, GroupService, TagService, EndpointProvider, Notifications, Authentication) { if (!$scope.applicationState.application.endpointManagement) { $state.go('portainer.endpoints'); @@ -13,13 +13,16 @@ function ($q, $scope, $state, $transition$, $filter, clipboard, EndpointService, $scope.state = { uploadInProgress: false, actionInProgress: false, - deploymentTab: 0 + deploymentTab: 0, + allowCreate: Authentication.isAdmin() }; $scope.formValues = { SecurityFormData: new EndpointSecurityFormData() }; + + $scope.copyEdgeAgentDeploymentCommand = function() { if ($scope.state.deploymentTab === 0) { clipboard.copyText('docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/volumes:/var/lib/docker/volumes -v /:/host --restart always -e EDGE=1 -e EDGE_ID=' + $scope.randomEdgeID + ' -e EDGE_KEY=' + $scope.endpoint.EdgeKey +' -e CAP_HOST_MANAGEMENT=1 -v portainer_agent_data:/data --name portainer_edge_agent portainer/agent'); @@ -34,6 +37,20 @@ function ($q, $scope, $state, $transition$, $filter, clipboard, EndpointService, $('#copyNotificationEdgeKey').show().fadeOut(2500); }; + $scope.onCreateTag = function onCreateTag(tagName) { + return $async(onCreateTagAsync, tagName); + } + + async function onCreateTagAsync(tagName) { + try { + const tag = await TagService.createTag(tagName); + $scope.availableTags = $scope.availableTags.concat(tag); + $scope.endpoint.TagIds = $scope.endpoint.TagIds.concat(tag.Id); + } catch(err) { + Notifications.error('Failue', err, 'Unable to create tag'); + } + } + $scope.updateEndpoint = function() { var endpoint = $scope.endpoint; var securityData = $scope.formValues.SecurityFormData; @@ -120,4 +137,4 @@ function ($q, $scope, $state, $transition$, $filter, clipboard, EndpointService, } initView(); -}]); +}); diff --git a/app/portainer/views/groups/create/createGroupController.js b/app/portainer/views/groups/create/createGroupController.js index 109e70ca1..9308c93b0 100644 --- a/app/portainer/views/groups/create/createGroupController.js +++ b/app/portainer/views/groups/create/createGroupController.js @@ -1,8 +1,7 @@ import {EndpointGroupDefaultModel} from '../../../models/group'; angular.module('portainer.app') -.controller('CreateGroupController', ['$q', '$scope', '$state', 'GroupService', 'EndpointService', 'TagService', 'Notifications', -function ($q, $scope, $state, GroupService, EndpointService, TagService, Notifications) { +.controller('CreateGroupController', function CreateGroupController($async, $scope, $state, GroupService, TagService, Notifications) { $scope.state = { actionInProgress: false @@ -31,6 +30,20 @@ function ($q, $scope, $state, GroupService, EndpointService, TagService, Notific }); }; + $scope.onCreateTag = function onCreateTag(tagName) { + return $async(onCreateTagAsync, tagName); + } + + async function onCreateTagAsync(tagName) { + try { + const tag = await TagService.createTag(tagName); + $scope.availableTags = $scope.availableTags.concat(tag); + $scope.model.TagIds = $scope.model.TagIds.concat(tag.Id); + } catch(err) { + Notifications.error('Failue', err, 'Unable to create tag'); + } + } + function initView() { TagService.tags() .then((tags) => { @@ -45,4 +58,4 @@ function ($q, $scope, $state, GroupService, EndpointService, TagService, Notific } initView(); -}]); +}); diff --git a/app/portainer/views/groups/create/creategroup.html b/app/portainer/views/groups/create/creategroup.html index 480d04f81..e89b01e08 100644 --- a/app/portainer/views/groups/create/creategroup.html +++ b/app/portainer/views/groups/create/creategroup.html @@ -21,6 +21,7 @@ form-action="create" form-action-label="Create the group" action-in-progress="state.actionInProgress" + on-create-tag="onCreateTag" > diff --git a/app/portainer/views/groups/edit/group.html b/app/portainer/views/groups/edit/group.html index 7dca1c807..25b8f19a0 100644 --- a/app/portainer/views/groups/edit/group.html +++ b/app/portainer/views/groups/edit/group.html @@ -21,6 +21,7 @@ form-action="update" form-action-label="Update the group" action-in-progress="state.actionInProgress" + on-create-tag="onCreateTag" > diff --git a/app/portainer/views/groups/edit/groupController.js b/app/portainer/views/groups/edit/groupController.js index d173e40b1..a9bcf1812 100644 --- a/app/portainer/views/groups/edit/groupController.js +++ b/app/portainer/views/groups/edit/groupController.js @@ -1,6 +1,5 @@ -angular.module('portainer.app') -.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'TagService', 'Notifications', -function ($q, $scope, $state, $transition$, GroupService, TagService, Notifications) { +angular.module('portainer.app').controller('GroupController', +function GroupController($q, $async, $scope, $state, $transition$, GroupService, TagService, Notifications) { $scope.state = { actionInProgress: false @@ -23,6 +22,20 @@ function ($q, $scope, $state, $transition$, GroupService, TagService, Notificati }); }; + $scope.onCreateTag = function onCreateTag(tagName) { + return $async(onCreateTagAsync, tagName); + } + + async function onCreateTagAsync(tagName) { + try { + const tag = await TagService.createTag(tagName); + $scope.availableTags = $scope.availableTags.concat(tag); + $scope.group.TagIds = $scope.group.TagIds.concat(tag.Id); + } catch(err) { + Notifications.error('Failue', err, 'Unable to create tag'); + } + } + function initView() { var groupId = $transition$.params().id; @@ -41,4 +54,4 @@ function ($q, $scope, $state, $transition$, GroupService, TagService, Notificati } initView(); -}]); +}); From df13f3b4ccab69bb8b6c7579cc27907c52556933 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 8 Apr 2020 12:03:52 +0300 Subject: [PATCH 08/40] chore(yarn): add start:client script back (#3691) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 050de62d3..f8cb84f4d 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "clean": "grunt clean:all", "start": "grunt clean:all && grunt start", "start:server": "grunt clean:server && grunt start:server", + "start:client": "grunt clean:client && grunt start:client", "dev:client": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.develop.js", "build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer", "clean:all": "grunt clean:all" From 2542d30a09f5e9403d9a1f8e175dc0f0e3358d2c Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 8 Apr 2020 12:14:50 +0300 Subject: [PATCH 09/40] feat(endpoints): filter by ids and/or tag ids (#3690) * feat(endpoints): add filter by tagIds * refactor(endpoints): change endpoints service to query by tagIds * fix(endpoints): filter by tags * feat(endpoints): filter by endpoint groups tags * feat(endpoints): filter by ids --- api/http/handler/endpoints/endpoint_list.go | 69 +++++++++++++++++++ app/portainer/services/api/endpointService.js | 4 +- .../views/endpoints/endpointsController.js | 4 +- app/portainer/views/home/homeController.js | 4 +- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index 1d98296bf..c68f8d9ec 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -30,6 +30,12 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true) endpointType, _ := request.RetrieveNumericQueryParameter(r, "type", true) + var tagIDs []portainer.TagID + request.RetrieveJSONQueryParameter(r, "tagIds", &tagIDs, true) + + var endpointIDs []portainer.EndpointID + request.RetrieveJSONQueryParameter(r, "endpointIds", &endpointIDs, true) + endpointGroups, err := handler.EndpointGroupService.EndpointGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err} @@ -47,6 +53,10 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext) + if endpointIDs != nil { + filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs) + } + if groupID != 0 { filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID)) } @@ -67,6 +77,10 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht filteredEndpoints = filterEndpointsByType(filteredEndpoints, portainer.EndpointType(endpointType)) } + if tagIDs != nil { + filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, tagIDs, endpointGroups) + } + filteredEndpointCount := len(filteredEndpoints) paginatedEndpoints := paginateEndpoints(filteredEndpoints, start, limit) @@ -187,3 +201,58 @@ func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer. } return tags } + +func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup) []portainer.Endpoint { + filteredEndpoints := make([]portainer.Endpoint, 0) + + for _, endpoint := range endpoints { + missingTags := make(map[portainer.TagID]bool) + for _, tagID := range tagIDs { + missingTags[tagID] = true + } + for _, tagID := range endpoint.TagIDs { + if missingTags[tagID] { + delete(missingTags, tagID) + } + } + missingTags = endpointGroupHasTags(endpoint.GroupID, endpointGroups, missingTags) + if len(missingTags) == 0 { + filteredEndpoints = append(filteredEndpoints, endpoint) + } + } + return filteredEndpoints +} + +func endpointGroupHasTags(groupID portainer.EndpointGroupID, groups []portainer.EndpointGroup, missingTags map[portainer.TagID]bool) map[portainer.TagID]bool { + var endpointGroup portainer.EndpointGroup + for _, group := range groups { + if group.ID == groupID { + endpointGroup = group + break + } + } + for _, tagID := range endpointGroup.TagIDs { + if missingTags[tagID] { + delete(missingTags, tagID) + } + } + return missingTags +} + +func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint { + filteredEndpoints := make([]portainer.Endpoint, 0) + + idsSet := make(map[portainer.EndpointID]bool) + for _, id := range ids { + idsSet[id] = true + } + + for _, endpoint := range endpoints { + if idsSet[endpoint.ID] { + filteredEndpoints = append(filteredEndpoints, endpoint) + } + } + + return filteredEndpoints + +} diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 6de03711d..7c60887fc 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -8,8 +8,8 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { return Endpoints.get({id: endpointID}).$promise; }; - service.endpoints = function(start, limit, search) { - return Endpoints.query({start, limit, search}).$promise; + service.endpoints = function(start, limit, { search, type, tagIds, endpointIds } = {}) { + return Endpoints.query({ start, limit, search, type, tagIds, endpointIds }).$promise; }; service.snapshotEndpoints = function() { diff --git a/app/portainer/views/endpoints/endpointsController.js b/app/portainer/views/endpoints/endpointsController.js index 3b2313700..2f4f1d268 100644 --- a/app/portainer/views/endpoints/endpointsController.js +++ b/app/portainer/views/endpoints/endpointsController.js @@ -22,10 +22,10 @@ function ($q, $scope, $state, EndpointService, GroupService, EndpointHelper, Not }; $scope.getPaginatedEndpoints = getPaginatedEndpoints; - function getPaginatedEndpoints(lastId, limit, filter) { + function getPaginatedEndpoints(lastId, limit, search) { const deferred = $q.defer(); $q.all({ - endpoints: EndpointService.endpoints(lastId, limit, filter), + endpoints: EndpointService.endpoints(lastId, limit, { search }), groups: GroupService.groups() }) .then(function success(data) { diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index 255c71146..319fd5a08 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -146,10 +146,10 @@ angular.module('portainer.app') } $scope.getPaginatedEndpoints = getPaginatedEndpoints; - function getPaginatedEndpoints(lastId, limit, filter) { + function getPaginatedEndpoints(lastId, limit, search) { const deferred = $q.defer(); $q.all({ - endpoints: EndpointService.endpoints(lastId, limit, filter), + endpoints: EndpointService.endpoints(lastId, limit, {search}), groups: GroupService.groups() }) .then(function success(data) { From 6da38d466b02632a66e3496fe135f0ce9c3d6393 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 9 Apr 2020 00:26:11 +0300 Subject: [PATCH 10/40] refactor(project): sort portainer types and interface definitions (#3694) * refactor(portainer): sort types * style(portainer): add comment about role service * refactor(portainer): sort interface types * refactor(portainer): sort enums --- api/portainer.go | 1340 +++++++++++++++++++++++----------------------- 1 file changed, 671 insertions(+), 669 deletions(-) diff --git a/api/portainer.go b/api/portainer.go index f0339677d..acf16961b 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -3,10 +3,33 @@ package portainer import "time" type ( - // Pair defines a key/value string pair - Pair struct { - Name string `json:"name"` - Value string `json:"value"` + // AccessPolicy represent a policy that can be associated to a user or team + AccessPolicy struct { + RoleID RoleID `json:"RoleId"` + } + + // APIOperationAuthorizationRequest represent an request for the authorization to execute an API operation + APIOperationAuthorizationRequest struct { + Path string + Method string + Authorizations Authorizations + } + + // AuthenticationMethod represents the authentication method used to authenticate a user + AuthenticationMethod int + + // Authorization represents an authorization associated to an operation + Authorization string + + // Authorizations represents a set of authorizations associated to a role + Authorizations map[Authorization]bool + + // AzureCredentials represents the credentials used to connect to an Azure + // environment. + AzureCredentials struct { + ApplicationID string `json:"ApplicationID"` + TenantID string `json:"TenantID"` + AuthenticationKey string `json:"AuthenticationKey"` } // CLIFlags represents the available flags on the CLI @@ -39,196 +62,18 @@ type ( SnapshotInterval *string } - // Status represents the application status - Status struct { - Authentication bool `json:"Authentication"` - EndpointManagement bool `json:"EndpointManagement"` - Snapshot bool `json:"Snapshot"` - Analytics bool `json:"Analytics"` - Version string `json:"Version"` + // CLIService represents a service for managing CLI + CLIService interface { + ParseFlags(version string) (*CLIFlags, error) + ValidateFlags(flags *CLIFlags) error } - // LDAPSettings represents the settings used to connect to a LDAP server - LDAPSettings struct { - AnonymousMode bool `json:"AnonymousMode"` - ReaderDN string `json:"ReaderDN"` - Password string `json:"Password,omitempty"` - URL string `json:"URL"` - TLSConfig TLSConfiguration `json:"TLSConfig"` - StartTLS bool `json:"StartTLS"` - SearchSettings []LDAPSearchSettings `json:"SearchSettings"` - GroupSearchSettings []LDAPGroupSearchSettings `json:"GroupSearchSettings"` - AutoCreateUsers bool `json:"AutoCreateUsers"` - } - - // OAuthSettings represents the settings used to authorize with an authorization server - OAuthSettings struct { - ClientID string `json:"ClientID"` - ClientSecret string `json:"ClientSecret,omitempty"` - AccessTokenURI string `json:"AccessTokenURI"` - AuthorizationURI string `json:"AuthorizationURI"` - ResourceURI string `json:"ResourceURI"` - RedirectURI string `json:"RedirectURI"` - UserIdentifier string `json:"UserIdentifier"` - Scopes string `json:"Scopes"` - OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"` - DefaultTeamID TeamID `json:"DefaultTeamID"` - } - - // TLSConfiguration represents a TLS configuration - TLSConfiguration struct { - TLS bool `json:"TLS"` - TLSSkipVerify bool `json:"TLSSkipVerify"` - TLSCACertPath string `json:"TLSCACert,omitempty"` - TLSCertPath string `json:"TLSCert,omitempty"` - TLSKeyPath string `json:"TLSKey,omitempty"` - } - - // LDAPSearchSettings represents settings used to search for users in a LDAP server - LDAPSearchSettings struct { - BaseDN string `json:"BaseDN"` - Filter string `json:"Filter"` - UserNameAttribute string `json:"UserNameAttribute"` - } - - // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server - LDAPGroupSearchSettings struct { - GroupBaseDN string `json:"GroupBaseDN"` - GroupFilter string `json:"GroupFilter"` - GroupAttribute string `json:"GroupAttribute"` - } - - // Settings represents the application settings - Settings struct { - LogoURL string `json:"LogoURL"` - BlackListedLabels []Pair `json:"BlackListedLabels"` - AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` - LDAPSettings LDAPSettings `json:"LDAPSettings"` - OAuthSettings OAuthSettings `json:"OAuthSettings"` - AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` - SnapshotInterval string `json:"SnapshotInterval"` - TemplatesURL string `json:"TemplatesURL"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` - - // Deprecated fields - DisplayDonationHeader bool - DisplayExternalContributors bool - } - - // User represents a user account - User struct { - ID UserID `json:"Id"` - Username string `json:"Username"` - Password string `json:"Password,omitempty"` - Role UserRole `json:"Role"` - PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"` - EndpointAuthorizations EndpointAuthorizations `json:"EndpointAuthorizations"` - } - - // UserID represents a user identifier - UserID int - - // UserRole represents the role of a user. It can be either an administrator - // or a regular user - UserRole int - - // AuthenticationMethod represents the authentication method used to authenticate a user - AuthenticationMethod int - - // Team represents a list of user accounts - Team struct { - ID TeamID `json:"Id"` - Name string `json:"Name"` - } - - // TeamID represents a team identifier - TeamID int - - // TeamMembership represents a membership association between a user and a team - TeamMembership struct { - ID TeamMembershipID `json:"Id"` - UserID UserID `json:"UserID"` - TeamID TeamID `json:"TeamID"` - Role MembershipRole `json:"Role"` - } - - // TeamMembershipID represents a team membership identifier - TeamMembershipID int - - // MembershipRole represents the role of a user within a team - MembershipRole int - - // TokenData represents the data embedded in a JWT token - TokenData struct { - ID UserID - Username string - Role UserRole - } - - // StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier) - StackID int - - // StackType represents the type of the stack (compose v2, stack deploy v3) - StackType int - - // Stack represents a Docker stack created via docker stack deploy - Stack struct { - ID StackID `json:"Id"` - Name string `json:"Name"` - Type StackType `json:"Type"` - EndpointID EndpointID `json:"EndpointId"` - SwarmID string `json:"SwarmId"` - EntryPoint string `json:"EntryPoint"` - Env []Pair `json:"Env"` - ResourceControl *ResourceControl `json:"ResourceControl"` - ProjectPath string - } - - // RegistryID represents a registry identifier - RegistryID int - - // RegistryType represents a type of registry - RegistryType int - - // GitlabRegistryData represents data required for gitlab registry to work - GitlabRegistryData struct { - ProjectID int `json:"ProjectId"` - InstanceURL string `json:"InstanceURL"` - ProjectPath string `json:"ProjectPath"` - } - - // Registry represents a Docker registry with all the info required - // to connect to it - Registry struct { - ID RegistryID `json:"Id"` - Type RegistryType `json:"Type"` - Name string `json:"Name"` - URL string `json:"URL"` - Authentication bool `json:"Authentication"` - Username string `json:"Username"` - Password string `json:"Password,omitempty"` - ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"` - Gitlab GitlabRegistryData `json:"Gitlab"` - UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` - TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` - - // Deprecated fields - // Deprecated in DBVersion == 18 - AuthorizedUsers []UserID `json:"AuthorizedUsers"` - AuthorizedTeams []TeamID `json:"AuthorizedTeams"` - } - - // RegistryManagementConfiguration represents a configuration that can be used to query - // the registry API via the registry management extension. - RegistryManagementConfiguration struct { - Type RegistryType `json:"Type"` - Authentication bool `json:"Authentication"` - Username string `json:"Username"` - Password string `json:"Password"` - TLSConfig TLSConfiguration `json:"TLSConfig"` + // DataStore defines the interface to manage the data + DataStore interface { + Open() error + Init() error + Close() error + MigrateData() error } // DockerHub represents all the required information to connect and use the @@ -239,14 +84,14 @@ type ( Password string `json:"Password,omitempty"` } - // EndpointID represents an endpoint identifier - EndpointID int - - // EndpointType represents the type of an endpoint - EndpointType int - - // EndpointStatus represents the status of an endpoint - EndpointStatus int + // EdgeSchedule represents a scheduled job that can run on Edge environments. + EdgeSchedule struct { + ID ScheduleID `json:"Id"` + CronExpression string `json:"CronExpression"` + Script string `json:"Script"` + Version int `json:"Version"` + Endpoints []EndpointID `json:"Endpoints"` + } // Endpoint represents a Docker endpoint with all the info required // to connect to it @@ -282,145 +127,19 @@ type ( Tags []string `json:"Tags"` } - // Authorization represents an authorization associated to an operation - Authorization string - - // Authorizations represents a set of authorizations associated to a role - Authorizations map[Authorization]bool - // EndpointAuthorizations represents the authorizations associated to a set of endpoints EndpointAuthorizations map[EndpointID]Authorizations - // APIOperationAuthorizationRequest represent an request for the authorization to execute an API operation - APIOperationAuthorizationRequest struct { - Path string - Method string - Authorizations Authorizations + // EndpointExtension represents a deprecated form of Portainer extension + // TODO: legacy extension management + EndpointExtension struct { + Type EndpointExtensionType `json:"Type"` + URL string `json:"URL"` } - // RoleID represents a role identifier - RoleID int - - // Role represents a set of authorizations that can be associated to a user or - // to a team. - Role struct { - ID RoleID `json:"Id"` - Name string `json:"Name"` - Description string `json:"Description"` - Authorizations Authorizations `json:"Authorizations"` - Priority int `json:"Priority"` - } - - // AccessPolicy represent a policy that can be associated to a user or team - AccessPolicy struct { - RoleID RoleID `json:"RoleId"` - } - - // UserAccessPolicies represent the association of an access policy and a user - UserAccessPolicies map[UserID]AccessPolicy - // TeamAccessPolicies represent the association of an access policy and a team - TeamAccessPolicies map[TeamID]AccessPolicy - - // ScheduleID represents a schedule identifier. - ScheduleID int - - // JobType represents a job type - JobType int - - // ScriptExecutionJob represents a scheduled job that can execute a script via a privileged container - ScriptExecutionJob struct { - Endpoints []EndpointID - Image string - ScriptPath string - RetryCount int - RetryInterval int - } - - // SnapshotJob represents a scheduled job that can create endpoint snapshots - SnapshotJob struct{} - - // EndpointSyncJob represents a scheduled job that synchronize endpoints based on an external file - EndpointSyncJob struct{} - - // Schedule represents a scheduled job. - // It only contains a pointer to one of the JobRunner implementations - // based on the JobType. - // NOTE: The Recurring option is only used by ScriptExecutionJob at the moment - Schedule struct { - ID ScheduleID `json:"Id"` - Name string - CronExpression string - Recurring bool - Created int64 - JobType JobType - EdgeSchedule *EdgeSchedule - ScriptExecutionJob *ScriptExecutionJob - SnapshotJob *SnapshotJob - EndpointSyncJob *EndpointSyncJob - } - - // EdgeSchedule represents a scheduled job that can run on Edge environments. - EdgeSchedule struct { - ID ScheduleID `json:"Id"` - CronExpression string `json:"CronExpression"` - Script string `json:"Script"` - Version int `json:"Version"` - Endpoints []EndpointID `json:"Endpoints"` - } - - // WebhookID represents a webhook identifier. - WebhookID int - - // WebhookType represents the type of resource a webhook is related to - WebhookType int - - // Webhook represents a url webhook that can be used to update a service - Webhook struct { - ID WebhookID `json:"Id"` - Token string `json:"Token"` - ResourceID string `json:"ResourceId"` - EndpointID EndpointID `json:"EndpointId"` - WebhookType WebhookType `json:"Type"` - } - - // AzureCredentials represents the credentials used to connect to an Azure - // environment. - AzureCredentials struct { - ApplicationID string `json:"ApplicationID"` - TenantID string `json:"TenantID"` - AuthenticationKey string `json:"AuthenticationKey"` - } - - // Snapshot represents a snapshot of a specific endpoint at a specific time - Snapshot struct { - Time int64 `json:"Time"` - DockerVersion string `json:"DockerVersion"` - Swarm bool `json:"Swarm"` - TotalCPU int `json:"TotalCPU"` - TotalMemory int64 `json:"TotalMemory"` - RunningContainerCount int `json:"RunningContainerCount"` - StoppedContainerCount int `json:"StoppedContainerCount"` - HealthyContainerCount int `json:"HealthyContainerCount"` - UnhealthyContainerCount int `json:"UnhealthyContainerCount"` - VolumeCount int `json:"VolumeCount"` - ImageCount int `json:"ImageCount"` - ServiceCount int `json:"ServiceCount"` - StackCount int `json:"StackCount"` - SnapshotRaw SnapshotRaw `json:"SnapshotRaw"` - } - - // SnapshotRaw represents all the information related to a snapshot as returned by the Docker API - SnapshotRaw struct { - Containers interface{} `json:"Containers"` - Volumes interface{} `json:"Volumes"` - Networks interface{} `json:"Networks"` - Images interface{} `json:"Images"` - Info interface{} `json:"Info"` - Version interface{} `json:"Version"` - } - - // EndpointGroupID represents an endpoint group identifier - EndpointGroupID int + // EndpointExtensionType represents the type of an endpoint extension. Only + // one extension of each type can be associated to an endpoint + EndpointExtensionType int // EndpointGroup represents a group of endpoints EndpointGroup struct { @@ -442,19 +161,151 @@ type ( Tags []string `json:"Tags"` } - // EndpointExtension represents a deprecated form of Portainer extension - // TODO: legacy extension management - EndpointExtension struct { - Type EndpointExtensionType `json:"Type"` - URL string `json:"URL"` + // EndpointGroupID represents an endpoint group identifier + EndpointGroupID int + + // EndpointID represents an endpoint identifier + EndpointID int + + // EndpointStatus represents the status of an endpoint + EndpointStatus int + + // EndpointSyncJob represents a scheduled job that synchronize endpoints based on an external file + EndpointSyncJob struct{} + + // EndpointType represents the type of an endpoint + EndpointType int + + // Extension represents a Portainer extension + Extension struct { + ID ExtensionID `json:"Id"` + Enabled bool `json:"Enabled"` + Name string `json:"Name,omitempty"` + ShortDescription string `json:"ShortDescription,omitempty"` + Description string `json:"Description,omitempty"` + DescriptionURL string `json:"DescriptionURL,omitempty"` + Price string `json:"Price,omitempty"` + PriceDescription string `json:"PriceDescription,omitempty"` + Deal bool `json:"Deal,omitempty"` + Available bool `json:"Available,omitempty"` + License LicenseInformation `json:"License,omitempty"` + Version string `json:"Version"` + UpdateAvailable bool `json:"UpdateAvailable"` + ShopURL string `json:"ShopURL,omitempty"` + Images []string `json:"Images,omitempty"` + Logo string `json:"Logo,omitempty"` } - // EndpointExtensionType represents the type of an endpoint extension. Only - // one extension of each type can be associated to an endpoint - EndpointExtensionType int + // ExtensionID represents a extension identifier + ExtensionID int - // ResourceControlID represents a resource control identifier - ResourceControlID int + // GitlabRegistryData represents data required for gitlab registry to work + GitlabRegistryData struct { + ProjectID int `json:"ProjectId"` + InstanceURL string `json:"InstanceURL"` + ProjectPath string `json:"ProjectPath"` + } + + // JobType represents a job type + JobType int + + // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server + LDAPGroupSearchSettings struct { + GroupBaseDN string `json:"GroupBaseDN"` + GroupFilter string `json:"GroupFilter"` + GroupAttribute string `json:"GroupAttribute"` + } + + // LDAPSearchSettings represents settings used to search for users in a LDAP server + LDAPSearchSettings struct { + BaseDN string `json:"BaseDN"` + Filter string `json:"Filter"` + UserNameAttribute string `json:"UserNameAttribute"` + } + + // LDAPSettings represents the settings used to connect to a LDAP server + LDAPSettings struct { + AnonymousMode bool `json:"AnonymousMode"` + ReaderDN string `json:"ReaderDN"` + Password string `json:"Password,omitempty"` + URL string `json:"URL"` + TLSConfig TLSConfiguration `json:"TLSConfig"` + StartTLS bool `json:"StartTLS"` + SearchSettings []LDAPSearchSettings `json:"SearchSettings"` + GroupSearchSettings []LDAPGroupSearchSettings `json:"GroupSearchSettings"` + AutoCreateUsers bool `json:"AutoCreateUsers"` + } + + // LicenseInformation represents information about an extension license + LicenseInformation struct { + LicenseKey string `json:"LicenseKey,omitempty"` + Company string `json:"Company,omitempty"` + Expiration string `json:"Expiration,omitempty"` + Valid bool `json:"Valid,omitempty"` + } + + // MembershipRole represents the role of a user within a team + MembershipRole int + + // OAuthSettings represents the settings used to authorize with an authorization server + OAuthSettings struct { + ClientID string `json:"ClientID"` + ClientSecret string `json:"ClientSecret,omitempty"` + AccessTokenURI string `json:"AccessTokenURI"` + AuthorizationURI string `json:"AuthorizationURI"` + ResourceURI string `json:"ResourceURI"` + RedirectURI string `json:"RedirectURI"` + UserIdentifier string `json:"UserIdentifier"` + Scopes string `json:"Scopes"` + OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"` + DefaultTeamID TeamID `json:"DefaultTeamID"` + } + + // Pair defines a key/value string pair + Pair struct { + Name string `json:"name"` + Value string `json:"value"` + } + + // Registry represents a Docker registry with all the info required + // to connect to it + Registry struct { + ID RegistryID `json:"Id"` + Type RegistryType `json:"Type"` + Name string `json:"Name"` + URL string `json:"URL"` + Authentication bool `json:"Authentication"` + Username string `json:"Username"` + Password string `json:"Password,omitempty"` + ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"` + Gitlab GitlabRegistryData `json:"Gitlab"` + UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` + TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` + + // Deprecated fields + // Deprecated in DBVersion == 18 + AuthorizedUsers []UserID `json:"AuthorizedUsers"` + AuthorizedTeams []TeamID `json:"AuthorizedTeams"` + } + + // RegistryID represents a registry identifier + RegistryID int + + // RegistryManagementConfiguration represents a configuration that can be used to query + // the registry API via the registry management extension. + RegistryManagementConfiguration struct { + Type RegistryType `json:"Type"` + Authentication bool `json:"Authentication"` + Username string `json:"Username"` + Password string `json:"Password"` + TLSConfig TLSConfiguration `json:"TLSConfig"` + } + + // RegistryType represents a type of registry + RegistryType int + + // ResourceAccessLevel represents the level of control associated to a resource + ResourceAccessLevel int // ResourceControl represent a reference to a Docker resource with specific access controls ResourceControl struct { @@ -474,23 +325,132 @@ type ( AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"` } + // ResourceControlID represents a resource control identifier + ResourceControlID int + // ResourceControlType represents the type of resource associated to the resource control (volume, container, service...) ResourceControlType int - // UserResourceAccess represents the level of control on a resource for a specific user - UserResourceAccess struct { - UserID UserID `json:"UserId"` - AccessLevel ResourceAccessLevel `json:"AccessLevel"` + // Role represents a set of authorizations that can be associated to a user or + // to a team. + Role struct { + ID RoleID `json:"Id"` + Name string `json:"Name"` + Description string `json:"Description"` + Authorizations Authorizations `json:"Authorizations"` + Priority int `json:"Priority"` } - // TeamResourceAccess represents the level of control on a resource for a specific team - TeamResourceAccess struct { - TeamID TeamID `json:"TeamId"` - AccessLevel ResourceAccessLevel `json:"AccessLevel"` + // RoleID represents a role identifier + RoleID int + + // Schedule represents a scheduled job. + // It only contains a pointer to one of the JobRunner implementations + // based on the JobType. + // NOTE: The Recurring option is only used by ScriptExecutionJob at the moment + Schedule struct { + ID ScheduleID `json:"Id"` + Name string + CronExpression string + Recurring bool + Created int64 + JobType JobType + EdgeSchedule *EdgeSchedule + ScriptExecutionJob *ScriptExecutionJob + SnapshotJob *SnapshotJob + EndpointSyncJob *EndpointSyncJob } - // TagID represents a tag identifier - TagID int + // ScheduleID represents a schedule identifier. + ScheduleID int + + // ScriptExecutionJob represents a scheduled job that can execute a script via a privileged container + ScriptExecutionJob struct { + Endpoints []EndpointID + Image string + ScriptPath string + RetryCount int + RetryInterval int + } + + // Settings represents the application settings + Settings struct { + LogoURL string `json:"LogoURL"` + BlackListedLabels []Pair `json:"BlackListedLabels"` + AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` + LDAPSettings LDAPSettings `json:"LDAPSettings"` + OAuthSettings OAuthSettings `json:"OAuthSettings"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + SnapshotInterval string `json:"SnapshotInterval"` + TemplatesURL string `json:"TemplatesURL"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` + + // Deprecated fields + DisplayDonationHeader bool + DisplayExternalContributors bool + } + + // Snapshot represents a snapshot of a specific endpoint at a specific time + Snapshot struct { + Time int64 `json:"Time"` + DockerVersion string `json:"DockerVersion"` + Swarm bool `json:"Swarm"` + TotalCPU int `json:"TotalCPU"` + TotalMemory int64 `json:"TotalMemory"` + RunningContainerCount int `json:"RunningContainerCount"` + StoppedContainerCount int `json:"StoppedContainerCount"` + HealthyContainerCount int `json:"HealthyContainerCount"` + UnhealthyContainerCount int `json:"UnhealthyContainerCount"` + VolumeCount int `json:"VolumeCount"` + ImageCount int `json:"ImageCount"` + ServiceCount int `json:"ServiceCount"` + StackCount int `json:"StackCount"` + SnapshotRaw SnapshotRaw `json:"SnapshotRaw"` + } + + // SnapshotJob represents a scheduled job that can create endpoint snapshots + SnapshotJob struct{} + + // SnapshotRaw represents all the information related to a snapshot as returned by the Docker API + SnapshotRaw struct { + Containers interface{} `json:"Containers"` + Volumes interface{} `json:"Volumes"` + Networks interface{} `json:"Networks"` + Images interface{} `json:"Images"` + Info interface{} `json:"Info"` + Version interface{} `json:"Version"` + } + + // Stack represents a Docker stack created via docker stack deploy + Stack struct { + ID StackID `json:"Id"` + Name string `json:"Name"` + Type StackType `json:"Type"` + EndpointID EndpointID `json:"EndpointId"` + SwarmID string `json:"SwarmId"` + EntryPoint string `json:"EntryPoint"` + Env []Pair `json:"Env"` + ResourceControl *ResourceControl `json:"ResourceControl"` + ProjectPath string + } + + // StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier) + StackID int + + // StackType represents the type of the stack (compose v2, stack deploy v3) + StackType int + + // Status represents the application status + Status struct { + Authentication bool `json:"Authentication"` + EndpointManagement bool `json:"EndpointManagement"` + Snapshot bool `json:"Snapshot"` + Analytics bool `json:"Analytics"` + Version string `json:"Version"` + } // Tag represents a tag that can be associated to a resource Tag struct { @@ -498,11 +458,37 @@ type ( Name string `json:"Name"` } - // TemplateID represents a template identifier - TemplateID int + // TagID represents a tag identifier + TagID int - // TemplateType represents the type of a template - TemplateType int + // Team represents a list of user accounts + Team struct { + ID TeamID `json:"Id"` + Name string `json:"Name"` + } + + // TeamAccessPolicies represent the association of an access policy and a team + TeamAccessPolicies map[TeamID]AccessPolicy + + // TeamID represents a team identifier + TeamID int + + // TeamMembership represents a membership association between a user and a team + TeamMembership struct { + ID TeamMembershipID `json:"Id"` + UserID UserID `json:"UserID"` + TeamID TeamID `json:"TeamID"` + Role MembershipRole `json:"Role"` + } + + // TeamMembershipID represents a team membership identifier + TeamMembershipID int + + // TeamResourceAccess represents the level of control on a resource for a specific team + TeamResourceAccess struct { + TeamID TeamID `json:"TeamId"` + AccessLevel ResourceAccessLevel `json:"AccessLevel"` + } // Template represents an application template Template struct { @@ -550,19 +536,6 @@ type ( Select []TemplateEnvSelect `json:"select,omitempty"` } - // TemplateVolume represents a template volume configuration - TemplateVolume struct { - Container string `json:"container"` - Bind string `json:"bind,omitempty"` - ReadOnly bool `json:"readonly,omitempty"` - } - - // TemplateRepository represents the git repository configuration for a template - TemplateRepository struct { - URL string `json:"url"` - StackFile string `json:"stackfile"` - } - // TemplateEnvSelect represents text/value pair that will be displayed as a choice for the // template user TemplateEnvSelect struct { @@ -571,42 +544,43 @@ type ( Default bool `json:"default"` } - // ResourceAccessLevel represents the level of control associated to a resource - ResourceAccessLevel int + // TemplateID represents a template identifier + TemplateID int + + // TemplateRepository represents the git repository configuration for a template + TemplateRepository struct { + URL string `json:"url"` + StackFile string `json:"stackfile"` + } + + // TemplateType represents the type of a template + TemplateType int + + // TemplateVolume represents a template volume configuration + TemplateVolume struct { + Container string `json:"container"` + Bind string `json:"bind,omitempty"` + ReadOnly bool `json:"readonly,omitempty"` + } + + // TLSConfiguration represents a TLS configuration + TLSConfiguration struct { + TLS bool `json:"TLS"` + TLSSkipVerify bool `json:"TLSSkipVerify"` + TLSCACertPath string `json:"TLSCACert,omitempty"` + TLSCertPath string `json:"TLSCert,omitempty"` + TLSKeyPath string `json:"TLSKey,omitempty"` + } // TLSFileType represents a type of TLS file required to connect to a Docker endpoint. // It can be either a TLS CA file, a TLS certificate file or a TLS key file TLSFileType int - // ExtensionID represents a extension identifier - ExtensionID int - - // Extension represents a Portainer extension - Extension struct { - ID ExtensionID `json:"Id"` - Enabled bool `json:"Enabled"` - Name string `json:"Name,omitempty"` - ShortDescription string `json:"ShortDescription,omitempty"` - Description string `json:"Description,omitempty"` - DescriptionURL string `json:"DescriptionURL,omitempty"` - Price string `json:"Price,omitempty"` - PriceDescription string `json:"PriceDescription,omitempty"` - Deal bool `json:"Deal,omitempty"` - Available bool `json:"Available,omitempty"` - License LicenseInformation `json:"License,omitempty"` - Version string `json:"Version"` - UpdateAvailable bool `json:"UpdateAvailable"` - ShopURL string `json:"ShopURL,omitempty"` - Images []string `json:"Images,omitempty"` - Logo string `json:"Logo,omitempty"` - } - - // LicenseInformation represents information about an extension license - LicenseInformation struct { - LicenseKey string `json:"LicenseKey,omitempty"` - Company string `json:"Company,omitempty"` - Expiration string `json:"Expiration,omitempty"` - Valid bool `json:"Valid,omitempty"` + // TokenData represents the data embedded in a JWT token + TokenData struct { + ID UserID + Username string + Role UserRole } // TunnelDetails represents information associated to a tunnel @@ -623,64 +597,72 @@ type ( PrivateKeySeed string `json:"PrivateKeySeed"` } - // CLIService represents a service for managing CLI - CLIService interface { - ParseFlags(version string) (*CLIFlags, error) - ValidateFlags(flags *CLIFlags) error + // User represents a user account + User struct { + ID UserID `json:"Id"` + Username string `json:"Username"` + Password string `json:"Password,omitempty"` + Role UserRole `json:"Role"` + PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"` + EndpointAuthorizations EndpointAuthorizations `json:"EndpointAuthorizations"` } - // DataStore defines the interface to manage the data - DataStore interface { - Open() error - Init() error - Close() error - MigrateData() error + // UserAccessPolicies represent the association of an access policy and a user + UserAccessPolicies map[UserID]AccessPolicy + + // UserID represents a user identifier + UserID int + + // UserResourceAccess represents the level of control on a resource for a specific user + UserResourceAccess struct { + UserID UserID `json:"UserId"` + AccessLevel ResourceAccessLevel `json:"AccessLevel"` } - // Server defines the interface to serve the API - Server interface { - Start() error + // UserRole represents the role of a user. It can be either an administrator + // or a regular user + UserRole int + + // Webhook represents a url webhook that can be used to update a service + Webhook struct { + ID WebhookID `json:"Id"` + Token string `json:"Token"` + ResourceID string `json:"ResourceId"` + EndpointID EndpointID `json:"EndpointId"` + WebhookType WebhookType `json:"Type"` } - // UserService represents a service for managing user data - UserService interface { - User(ID UserID) (*User, error) - UserByUsername(username string) (*User, error) - Users() ([]User, error) - UsersByRole(role UserRole) ([]User, error) - CreateUser(user *User) error - UpdateUser(ID UserID, user *User) error - DeleteUser(ID UserID) error + // WebhookID represents a webhook identifier. + WebhookID int + + // WebhookType represents the type of resource a webhook is related to + WebhookType int + + // ComposeStackManager represents a service to manage Compose stacks + ComposeStackManager interface { + Up(stack *Stack, endpoint *Endpoint) error + Down(stack *Stack, endpoint *Endpoint) error } - RoleService interface { - Role(ID RoleID) (*Role, error) - Roles() ([]Role, error) - CreateRole(role *Role) error - UpdateRole(ID RoleID, role *Role) error + // CryptoService represents a service for encrypting/hashing data + CryptoService interface { + Hash(data string) (string, error) + CompareHashAndData(hash string, data string) error } - // TeamService represents a service for managing user data - TeamService interface { - Team(ID TeamID) (*Team, error) - TeamByName(name string) (*Team, error) - Teams() ([]Team, error) - CreateTeam(team *Team) error - UpdateTeam(ID TeamID, team *Team) error - DeleteTeam(ID TeamID) error + // DigitalSignatureService represents a service to manage digital signatures + DigitalSignatureService interface { + ParseKeyPair(private, public []byte) error + GenerateKeyPair() ([]byte, []byte, error) + EncodedPublicKey() string + PEMHeaders() (string, string) + CreateSignature(message string) (string, error) } - // TeamMembershipService represents a service for managing team membership data - TeamMembershipService interface { - TeamMembership(ID TeamMembershipID) (*TeamMembership, error) - TeamMemberships() ([]TeamMembership, error) - TeamMembershipsByUserID(userID UserID) ([]TeamMembership, error) - TeamMembershipsByTeamID(teamID TeamID) ([]TeamMembership, error) - CreateTeamMembership(membership *TeamMembership) error - UpdateTeamMembership(ID TeamMembershipID, membership *TeamMembership) error - DeleteTeamMembership(ID TeamMembershipID) error - DeleteTeamMembershipByUserID(userID UserID) error - DeleteTeamMembershipByTeamID(teamID TeamID) error + // DockerHubService represents a service for managing the DockerHub object + DockerHubService interface { + DockerHub() (*DockerHub, error) + UpdateDockerHub(registry *DockerHub) error } // EndpointService represents a service for managing endpoint data @@ -703,96 +685,14 @@ type ( DeleteEndpointGroup(ID EndpointGroupID) error } - // RegistryService represents a service for managing registry data - RegistryService interface { - Registry(ID RegistryID) (*Registry, error) - Registries() ([]Registry, error) - CreateRegistry(registry *Registry) error - UpdateRegistry(ID RegistryID, registry *Registry) error - DeleteRegistry(ID RegistryID) error - } - - // StackService represents a service for managing stack data - StackService interface { - Stack(ID StackID) (*Stack, error) - StackByName(name string) (*Stack, error) - Stacks() ([]Stack, error) - CreateStack(stack *Stack) error - UpdateStack(ID StackID, stack *Stack) error - DeleteStack(ID StackID) error - GetNextIdentifier() int - } - - // DockerHubService represents a service for managing the DockerHub object - DockerHubService interface { - DockerHub() (*DockerHub, error) - UpdateDockerHub(registry *DockerHub) error - } - - // SettingsService represents a service for managing application settings - SettingsService interface { - Settings() (*Settings, error) - UpdateSettings(settings *Settings) error - } - - // VersionService represents a service for managing version data - VersionService interface { - DBVersion() (int, error) - StoreDBVersion(version int) error - } - - // TunnelServerService represents a service for managing data associated to the tunnel server - TunnelServerService interface { - Info() (*TunnelServerInfo, error) - UpdateInfo(info *TunnelServerInfo) error - } - - // WebhookService represents a service for managing webhook data. - WebhookService interface { - Webhooks() ([]Webhook, error) - Webhook(ID WebhookID) (*Webhook, error) - CreateWebhook(portainer *Webhook) error - WebhookByResourceID(resourceID string) (*Webhook, error) - WebhookByToken(token string) (*Webhook, error) - DeleteWebhook(serviceID WebhookID) error - } - - // ResourceControlService represents a service for managing resource control data - ResourceControlService interface { - ResourceControl(ID ResourceControlID) (*ResourceControl, error) - ResourceControlByResourceIDAndType(resourceID string, resourceType ResourceControlType) (*ResourceControl, error) - ResourceControls() ([]ResourceControl, error) - CreateResourceControl(rc *ResourceControl) error - UpdateResourceControl(ID ResourceControlID, resourceControl *ResourceControl) error - DeleteResourceControl(ID ResourceControlID) error - } - - // ScheduleService represents a service for managing schedule data - ScheduleService interface { - Schedule(ID ScheduleID) (*Schedule, error) - Schedules() ([]Schedule, error) - SchedulesByJobType(jobType JobType) ([]Schedule, error) - CreateSchedule(schedule *Schedule) error - UpdateSchedule(ID ScheduleID, schedule *Schedule) error - DeleteSchedule(ID ScheduleID) error - GetNextIdentifier() int - } - - // TagService represents a service for managing tag data - TagService interface { - Tags() ([]Tag, error) - Tag(ID TagID) (*Tag, error) - CreateTag(tag *Tag) error - DeleteTag(ID TagID) error - } - - // TemplateService represents a service for managing template data - TemplateService interface { - Templates() ([]Template, error) - Template(ID TemplateID) (*Template, error) - CreateTemplate(template *Template) error - UpdateTemplate(ID TemplateID, template *Template) error - DeleteTemplate(ID TemplateID) error + // ExtensionManager represents a service used to manage extensions + ExtensionManager interface { + FetchExtensionDefinitions() ([]Extension, error) + InstallExtension(extension *Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error + EnableExtension(extension *Extension, licenseKey string) error + DisableExtension(extension *Extension) error + UpdateExtension(extension *Extension, version string) error + StartExtensions() error } // ExtensionService represents a service for managing extension data @@ -803,27 +703,6 @@ type ( DeleteExtension(ID ExtensionID) error } - // CryptoService represents a service for encrypting/hashing data - CryptoService interface { - Hash(data string) (string, error) - CompareHashAndData(hash string, data string) error - } - - // DigitalSignatureService represents a service to manage digital signatures - DigitalSignatureService interface { - ParseKeyPair(private, public []byte) error - GenerateKeyPair() ([]byte, []byte, error) - EncodedPublicKey() string - PEMHeaders() (string, string) - CreateSignature(message string) (string, error) - } - - // JWTService represents a service for managing JWT tokens - JWTService interface { - GenerateToken(data *TokenData) (string, error) - ParseAndVerifyToken(token string) (*TokenData, error) - } - // FileService represents a service for managing files FileService interface { GetFileContent(filePath string) ([]byte, error) @@ -853,6 +732,12 @@ type ( ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error } + // JobRunner represents a service that can be used to run a job + JobRunner interface { + Run() + GetSchedule() *Schedule + } + // JobScheduler represents a service to run jobs on a periodic basis JobScheduler interface { ScheduleJob(runner JobRunner) error @@ -862,15 +747,15 @@ type ( Start() } - // JobRunner represents a service that can be used to run a job - JobRunner interface { - Run() - GetSchedule() *Schedule + // JobService represents a service to manage job execution on hosts + JobService interface { + ExecuteScript(endpoint *Endpoint, nodeName, image string, script []byte, schedule *Schedule) error } - // Snapshotter represents a service used to create endpoint snapshots - Snapshotter interface { - CreateSnapshot(endpoint *Endpoint) (*Snapshot, error) + // JWTService represents a service for managing JWT tokens + JWTService interface { + GenerateToken(data *TokenData) (string, error) + ParseAndVerifyToken(token string) (*TokenData, error) } // LDAPService represents a service used to authenticate users against a LDAP/AD @@ -880,33 +765,23 @@ type ( GetUserGroups(username string, settings *LDAPSettings) ([]string, error) } - // SwarmStackManager represents a service to manage Swarm stacks - SwarmStackManager interface { - Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) - Logout(endpoint *Endpoint) error - Deploy(stack *Stack, prune bool, endpoint *Endpoint) error - Remove(stack *Stack, endpoint *Endpoint) error + // RegistryService represents a service for managing registry data + RegistryService interface { + Registry(ID RegistryID) (*Registry, error) + Registries() ([]Registry, error) + CreateRegistry(registry *Registry) error + UpdateRegistry(ID RegistryID, registry *Registry) error + DeleteRegistry(ID RegistryID) error } - // ComposeStackManager represents a service to manage Compose stacks - ComposeStackManager interface { - Up(stack *Stack, endpoint *Endpoint) error - Down(stack *Stack, endpoint *Endpoint) error - } - - // JobService represents a service to manage job execution on hosts - JobService interface { - ExecuteScript(endpoint *Endpoint, nodeName, image string, script []byte, schedule *Schedule) error - } - - // ExtensionManager represents a service used to manage extensions - ExtensionManager interface { - FetchExtensionDefinitions() ([]Extension, error) - InstallExtension(extension *Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error - EnableExtension(extension *Extension, licenseKey string) error - DisableExtension(extension *Extension) error - UpdateExtension(extension *Extension, version string) error - StartExtensions() error + // ResourceControlService represents a service for managing resource control data + ResourceControlService interface { + ResourceControl(ID ResourceControlID) (*ResourceControl, error) + ResourceControlByResourceIDAndType(resourceID string, resourceType ResourceControlType) (*ResourceControl, error) + ResourceControls() ([]ResourceControl, error) + CreateResourceControl(rc *ResourceControl) error + UpdateResourceControl(ID ResourceControlID, resourceControl *ResourceControl) error + DeleteResourceControl(ID ResourceControlID) error } // ReverseTunnelService represensts a service used to manage reverse tunnel connections. @@ -920,6 +795,133 @@ type ( AddSchedule(endpointID EndpointID, schedule *EdgeSchedule) RemoveSchedule(scheduleID ScheduleID) } + + // RoleService represents a service for managing user roles + RoleService interface { + Role(ID RoleID) (*Role, error) + Roles() ([]Role, error) + CreateRole(role *Role) error + UpdateRole(ID RoleID, role *Role) error + } + + // ScheduleService represents a service for managing schedule data + ScheduleService interface { + Schedule(ID ScheduleID) (*Schedule, error) + Schedules() ([]Schedule, error) + SchedulesByJobType(jobType JobType) ([]Schedule, error) + CreateSchedule(schedule *Schedule) error + UpdateSchedule(ID ScheduleID, schedule *Schedule) error + DeleteSchedule(ID ScheduleID) error + GetNextIdentifier() int + } + + // SettingsService represents a service for managing application settings + SettingsService interface { + Settings() (*Settings, error) + UpdateSettings(settings *Settings) error + } + + // Server defines the interface to serve the API + Server interface { + Start() error + } + + // Snapshotter represents a service used to create endpoint snapshots + Snapshotter interface { + CreateSnapshot(endpoint *Endpoint) (*Snapshot, error) + } + + // StackService represents a service for managing stack data + StackService interface { + Stack(ID StackID) (*Stack, error) + StackByName(name string) (*Stack, error) + Stacks() ([]Stack, error) + CreateStack(stack *Stack) error + UpdateStack(ID StackID, stack *Stack) error + DeleteStack(ID StackID) error + GetNextIdentifier() int + } + + // SwarmStackManager represents a service to manage Swarm stacks + SwarmStackManager interface { + Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) + Logout(endpoint *Endpoint) error + Deploy(stack *Stack, prune bool, endpoint *Endpoint) error + Remove(stack *Stack, endpoint *Endpoint) error + } + + // TagService represents a service for managing tag data + TagService interface { + Tags() ([]Tag, error) + Tag(ID TagID) (*Tag, error) + CreateTag(tag *Tag) error + DeleteTag(ID TagID) error + } + + // TeamService represents a service for managing user data + TeamService interface { + Team(ID TeamID) (*Team, error) + TeamByName(name string) (*Team, error) + Teams() ([]Team, error) + CreateTeam(team *Team) error + UpdateTeam(ID TeamID, team *Team) error + DeleteTeam(ID TeamID) error + } + + // TeamMembershipService represents a service for managing team membership data + TeamMembershipService interface { + TeamMembership(ID TeamMembershipID) (*TeamMembership, error) + TeamMemberships() ([]TeamMembership, error) + TeamMembershipsByUserID(userID UserID) ([]TeamMembership, error) + TeamMembershipsByTeamID(teamID TeamID) ([]TeamMembership, error) + CreateTeamMembership(membership *TeamMembership) error + UpdateTeamMembership(ID TeamMembershipID, membership *TeamMembership) error + DeleteTeamMembership(ID TeamMembershipID) error + DeleteTeamMembershipByUserID(userID UserID) error + DeleteTeamMembershipByTeamID(teamID TeamID) error + } + + // TemplateService represents a service for managing template data + TemplateService interface { + Templates() ([]Template, error) + Template(ID TemplateID) (*Template, error) + CreateTemplate(template *Template) error + UpdateTemplate(ID TemplateID, template *Template) error + DeleteTemplate(ID TemplateID) error + } + + // TunnelServerService represents a service for managing data associated to the tunnel server + TunnelServerService interface { + Info() (*TunnelServerInfo, error) + UpdateInfo(info *TunnelServerInfo) error + } + + // UserService represents a service for managing user data + UserService interface { + User(ID UserID) (*User, error) + UserByUsername(username string) (*User, error) + Users() ([]User, error) + UsersByRole(role UserRole) ([]User, error) + CreateUser(user *User) error + UpdateUser(ID UserID, user *User) error + DeleteUser(ID UserID) error + } + + // VersionService represents a service for managing version data + VersionService interface { + DBVersion() (int, error) + StoreDBVersion(version int) error + } + + // WebhookService represents a service for managing webhook data. + WebhookService interface { + Webhooks() ([]Webhook, error) + Webhook(ID WebhookID) (*Webhook, error) + CreateWebhook(portainer *Webhook) error + WebhookByResourceID(resourceID string) (*Webhook, error) + WebhookByToken(token string) (*Webhook, error) + DeleteWebhook(serviceID WebhookID) error + } ) const ( @@ -959,12 +961,61 @@ const ( ) const ( - // TLSFileCA represents a TLS CA certificate file - TLSFileCA TLSFileType = iota - // TLSFileCert represents a TLS certificate file - TLSFileCert - // TLSFileKey represents a TLS key file - TLSFileKey + _ AuthenticationMethod = iota + // AuthenticationInternal represents the internal authentication method (authentication against Portainer API) + AuthenticationInternal + // AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server) + AuthenticationLDAP + //AuthenticationOAuth represents the OAuth authentication method (authentication against a authorization server) + AuthenticationOAuth +) + +const ( + _ EndpointExtensionType = iota + // StoridgeEndpointExtension represents the Storidge extension + StoridgeEndpointExtension +) + +const ( + _ EndpointStatus = iota + // EndpointStatusUp is used to represent an available endpoint + EndpointStatusUp + // EndpointStatusDown is used to represent an unavailable endpoint + EndpointStatusDown +) + +const ( + _ EndpointType = iota + // DockerEnvironment represents an endpoint connected to a Docker environment + DockerEnvironment + // AgentOnDockerEnvironment represents an endpoint connected to a Portainer agent deployed on a Docker environment + AgentOnDockerEnvironment + // AzureEnvironment represents an endpoint connected to an Azure environment + AzureEnvironment + // EdgeAgentEnvironment represents an endpoint connected to an Edge agent + EdgeAgentEnvironment +) + +const ( + _ ExtensionID = iota + // RegistryManagementExtension represents the registry management extension + RegistryManagementExtension + // OAuthAuthenticationExtension represents the OAuth authentication extension + OAuthAuthenticationExtension + // RBACExtension represents the RBAC extension + RBACExtension +) + +const ( + _ JobType = iota + // ScriptExecutionJobType is a non-system job used to execute a script against a list of + // endpoints via privileged containers + ScriptExecutionJobType + // SnapshotJobType is a system job used to create endpoint snapshots + SnapshotJobType + // EndpointSyncJobType is a system job used to synchronize endpoints from + // an external definition store + EndpointSyncJobType ) const ( @@ -976,21 +1027,15 @@ const ( ) const ( - _ UserRole = iota - // AdministratorRole represents an administrator user role - AdministratorRole - // StandardUserRole represents a regular user role - StandardUserRole -) - -const ( - _ AuthenticationMethod = iota - // AuthenticationInternal represents the internal authentication method (authentication against Portainer API) - AuthenticationInternal - // AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server) - AuthenticationLDAP - //AuthenticationOAuth represents the OAuth authentication method (authentication against a authorization server) - AuthenticationOAuth + _ RegistryType = iota + // QuayRegistry represents a Quay.io registry + QuayRegistry + // AzureRegistry represents an ACR registry + AzureRegistry + // CustomRegistry represents a custom registry + CustomRegistry + // GitlabRegistry represents a gitlab registry + GitlabRegistry ) const ( @@ -1017,24 +1062,6 @@ const ( ConfigResourceControl ) -const ( - _ EndpointExtensionType = iota - // StoridgeEndpointExtension represents the Storidge extension - StoridgeEndpointExtension -) - -const ( - _ EndpointType = iota - // DockerEnvironment represents an endpoint connected to a Docker environment - DockerEnvironment - // AgentOnDockerEnvironment represents an endpoint connected to a Portainer agent deployed on a Docker environment - AgentOnDockerEnvironment - // AzureEnvironment represents an endpoint connected to an Azure environment - AzureEnvironment - // EdgeAgentEnvironment represents an endpoint connected to an Edge agent - EdgeAgentEnvironment -) - const ( _ StackType = iota // DockerSwarmStack represents a stack managed via docker stack @@ -1054,11 +1081,20 @@ const ( ) const ( - _ EndpointStatus = iota - // EndpointStatusUp is used to represent an available endpoint - EndpointStatusUp - // EndpointStatusDown is used to represent an unavailable endpoint - EndpointStatusDown + // TLSFileCA represents a TLS CA certificate file + TLSFileCA TLSFileType = iota + // TLSFileCert represents a TLS certificate file + TLSFileCert + // TLSFileKey represents a TLS key file + TLSFileKey +) + +const ( + _ UserRole = iota + // AdministratorRole represents an administrator user role + AdministratorRole + // StandardUserRole represents a regular user role + StandardUserRole ) const ( @@ -1067,40 +1103,6 @@ const ( ServiceWebhook ) -const ( - _ ExtensionID = iota - // RegistryManagementExtension represents the registry management extension - RegistryManagementExtension - // OAuthAuthenticationExtension represents the OAuth authentication extension - OAuthAuthenticationExtension - // RBACExtension represents the RBAC extension - RBACExtension -) - -const ( - _ JobType = iota - // ScriptExecutionJobType is a non-system job used to execute a script against a list of - // endpoints via privileged containers - ScriptExecutionJobType - // SnapshotJobType is a system job used to create endpoint snapshots - SnapshotJobType - // EndpointSyncJobType is a system job used to synchronize endpoints from - // an external definition store - EndpointSyncJobType -) - -const ( - _ RegistryType = iota - // QuayRegistry represents a Quay.io registry - QuayRegistry - // AzureRegistry represents an ACR registry - AzureRegistry - // CustomRegistry represents a custom registry - CustomRegistry - // GitlabRegistry represents a gitlab registry - GitlabRegistry -) - const ( // EdgeAgentIdle represents an idle state for a tunnel connected to an Edge endpoint. EdgeAgentIdle string = "IDLE" From aeea88be36d67bfcc03bd9dab65bb59a27e80d6c Mon Sep 17 00:00:00 2001 From: Neil Cresswell Date: Thu, 9 Apr 2020 09:59:14 +1200 Subject: [PATCH 11/40] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 86363fab2..3c87a220e 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,10 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart ## Getting help -**NOTE**: You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/ +For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/ + +For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/ + * Issues: https://github.com/portainer/portainer/issues * FAQ: https://portainer.readthedocs.io/en/latest/faq.html From 18a38d597af77f6637bd86bdf9ea8e8f03c88eea Mon Sep 17 00:00:00 2001 From: Neil Cresswell Date: Thu, 9 Apr 2020 10:00:29 +1200 Subject: [PATCH 12/40] Update README.md --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 3c87a220e..a66d4985d 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,7 @@ For community support: You can find more information about Portainer's community **_Portainer_** has full support for the following Docker versions: -* Docker 1.10 to the latest version -* Standalone Docker Swarm >= 1.2.3 _(**NOTE:** Use of Standalone Docker Swarm is being discouraged since the introduction of built-in Swarm Mode in Docker. While older versions of Portainer had support for Standalone Docker Swarm, Portainer 1.17.0 and newer **do not** support it. However, the built-in Swarm Mode of Docker is fully supported.)_ - -Partial support for the following Docker versions (some features may not be available): - -* Docker 1.9 +Portainer supports N-2 Docker Versions. ## Licensing From 6663073be165ca90f430aa4b7b391857bf95d0de Mon Sep 17 00:00:00 2001 From: Neil Cresswell Date: Thu, 9 Apr 2020 10:01:20 +1200 Subject: [PATCH 13/40] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index a66d4985d..6149b69c3 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,7 @@ For community support: You can find more information about Portainer's community ## Limitations -**_Portainer_** has full support for the following Docker versions: - -Portainer supports N-2 Docker Versions. +Portainer supports "Current - 2 docker versions only. Prior versions may operate, however these are not supported. ## Licensing From cf5056d9c03b62d91a25c3b9127caac838695f98 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sat, 11 Apr 2020 00:54:53 +0300 Subject: [PATCH 14/40] chore(project): add prettier for code format (#3645) * chore(project): install prettier and lint-staged * chore(project): apply prettier to html too * chore(project): git ignore eslintcache * chore(project): add a comment about format script * chore(prettier): update printWidth * chore(prettier): remove useTabs option * chore(prettier): add HTML validation * refactor(prettier): fix closing tags * feat(prettier): define angular parser for html templates * style(prettier): run prettier on codebase Co-authored-by: Anthony Lapenna --- .gitignore | 3 +- .prettierrc | 13 + app/__module.js | 4 +- .../file-uploader/file-uploader-controller.js | 4 +- .../file-uploader/file-uploader.html | 7 +- .../components/file-uploader/file-uploader.js | 4 +- .../files-datatable/files-datatable.html | 45 +- .../files-datatable/files-datatable.js | 6 +- .../host-browser/host-browser-controller.js | 26 +- .../components/host-browser/host-browser.html | 10 +- .../components/host-browser/host-browser.js | 2 +- .../components/node-selector/node-selector.js | 4 +- .../node-selector/nodeSelector.html | 4 +- .../node-selector/nodeSelectorController.js | 36 +- .../volume-browser/volume-browser.js | 4 +- .../volume-browser/volumeBrowser.html | 9 +- .../volume-browser/volumeBrowserController.js | 236 +- app/agent/models/agent.js | 4 +- app/agent/rest/agent.js | 27 +- app/agent/rest/browse.js | 58 +- app/agent/rest/host.js | 11 +- app/agent/rest/ping.js | 20 +- app/agent/rest/v1/agent.js | 25 +- app/agent/rest/v1/browse.js | 56 +- app/agent/services/agentService.js | 16 +- app/agent/services/hostBrowserService.js | 21 +- app/agent/services/pingService.js | 2 +- app/agent/services/volumeBrowserService.js | 35 +- app/app.js | 77 +- app/azure/_module.js | 90 +- .../azure-endpoint-config.js | 4 +- .../azureEndpointConfig.html | 13 +- .../azure-sidebar-content.js | 2 +- .../containerGroupsDatatable.html | 32 +- .../containerGroupsDatatable.js | 4 +- app/azure/models/container_group.js | 24 +- app/azure/models/provider.js | 2 +- app/azure/rest/azure.js | 31 +- app/azure/rest/container_group.js | 76 +- app/azure/rest/location.js | 28 +- app/azure/rest/provider.js | 28 +- app/azure/rest/resource_group.js | 28 +- app/azure/rest/subscription.js | 28 +- app/azure/services/azureService.js | 114 +- app/azure/services/containerGroupService.js | 62 +- app/azure/services/locationService.js | 43 +- app/azure/services/providerService.js | 39 +- app/azure/services/resourceGroupService.js | 43 +- app/azure/services/subscriptionService.js | 43 +- .../containerInstancesController.js | 79 +- .../containerinstances.html | 10 +- .../createContainerInstanceController.js | 170 +- .../create/createcontainerinstance.html | 35 +- .../views/dashboard/dashboardController.js | 42 +- app/config.js | 61 +- app/constants.js | 51 +- app/docker/__module.js | 964 +-- .../container-capabilities.js | 4 +- .../containerCapabilities.html | 9 +- .../containerQuickActions.html | 27 +- .../containerQuickActions.js | 4 +- .../container-restart-policy-controller.js | 45 +- .../container-restart-policy.js | 11 +- .../dashboard-cluster-agent-info.js | 2 +- .../dashboardClusterAgentInfoController.js | 31 +- .../configs-datatable/configsDatatable.html | 38 +- .../configs-datatable/configsDatatable.js | 4 +- .../containerNetworksDatatable.html | 26 +- .../containerNetworksDatatable.js | 4 +- .../containerProcessesDatatable.html | 6 +- .../containerProcessesDatatable.js | 4 +- .../actions/containersDatatableActions.html | 68 +- .../actions/containersDatatableActions.js | 4 +- .../containersDatatableActionsController.js | 184 +- .../containersDatatable.html | 210 +- .../containersDatatable.js | 4 +- .../containersDatatableController.js | 367 +- .../events-datatable/eventsDatatable.html | 19 +- .../events-datatable/eventsDatatable.js | 4 +- .../host-jobs-datatable/jobsDatatable.html | 38 +- .../host-jobs-datatable/jobsDatatable.js | 6 +- .../jobsDatatableController.js | 257 +- .../images-datatable/imagesDatatable.html | 67 +- .../images-datatable/imagesDatatable.js | 4 +- .../imagesDatatableController.js | 123 +- .../macvlanNodesDatatable.html | 22 +- .../macvlanNodesDatatable.js | 4 +- .../networkRowContent.html | 18 +- .../network-row-content/networkRowContent.js | 37 +- .../networks-datatable/networksDatatable.html | 46 +- .../networks-datatable/networksDatatable.js | 4 +- .../networksDatatableController.js | 136 +- .../nodeTasksDatatable.html | 27 +- .../nodeTasksDatatable.js | 4 +- .../nodes-datatable/nodesDatatable.html | 29 +- .../nodes-datatable/nodesDatatable.js | 4 +- .../secrets-datatable/secretsDatatable.html | 36 +- .../secrets-datatable/secretsDatatable.js | 4 +- .../serviceTasksDatatable.html | 30 +- .../serviceTasksDatatable.js | 4 +- .../serviceTasksDatatableController.js | 155 +- .../actions/servicesDatatableActions.html | 19 +- .../actions/servicesDatatableActions.js | 4 +- .../servicesDatatableActionsController.js | 191 +- .../services-datatable/servicesDatatable.html | 55 +- .../services-datatable/servicesDatatable.js | 4 +- .../servicesDatatableController.js | 177 +- .../tasks-datatable/tasksDatatable.html | 46 +- .../tasks-datatable/tasksDatatable.js | 4 +- .../tasksDatatableController.js | 84 +- .../volumes-datatable/volumesDatatable.html | 46 +- .../volumes-datatable/volumesDatatable.js | 4 +- .../volumesDatatableController.js | 123 +- .../docker-sidebar-content.js | 4 +- .../dockerSidebarContent.html | 2 +- .../host-overview/host-overview.html | 13 +- .../components/host-overview/host-overview.js | 4 +- .../devices-panel/devices-panel.html | 6 +- .../devices-panel/devices-panel.js | 4 +- .../disks-panel/disks-panel.html | 6 +- .../disks-panel/disks-panel.js | 4 +- .../engine-details-panel.html | 8 +- .../engine-details-panel.js | 4 +- .../host-details-panel.html | 7 +- .../host-details-panel/host-details-panel.js | 4 +- .../node-availability-select-controller.js | 16 +- .../node-availability-select.html | 5 +- .../node-availability-select.js | 4 +- .../node-labels-table-controller.js | 7 +- .../node-labels-table/node-labels-table.html | 8 +- .../node-labels-table/node-labels-table.js | 4 +- .../swarm-node-details-panel-controller.js | 177 +- .../swarm-node-details-panel.html | 28 +- .../swarm-node-details-panel.js | 4 +- .../imageRegistry/por-image-registry.js | 14 +- .../imageRegistry/porImageRegistry.html | 33 +- .../porImageRegistryController.js | 6 +- .../components/log-viewer/log-viewer.js | 4 +- .../components/log-viewer/logViewer.html | 42 +- .../log-viewer/logViewerController.js | 82 +- .../network-macvlan-form.js | 6 +- .../networkMacvlanForm.html | 53 +- .../networkMacvlanFormController.js | 93 +- .../networkMacvlanFormModel.js | 4 +- .../volumesNFSForm/volumes-nfs-form.js | 4 +- .../volumesNFSForm/volumesNFSFormModel.js | 2 +- .../volumesNFSForm/volumesnfsForm.html | 31 +- app/docker/filters/filters.js | 561 +- app/docker/helpers/configHelper.js | 69 +- app/docker/helpers/constraintsHelper.js | 22 +- app/docker/helpers/containerHelper.js | 300 +- app/docker/helpers/imageHelper.js | 113 +- app/docker/helpers/infoHelper.js | 65 +- app/docker/helpers/labelHelper.js | 51 +- app/docker/helpers/logHelper.js | 36 +- app/docker/helpers/nodeHelper.js | 29 +- app/docker/helpers/secretHelper.js | 69 +- app/docker/helpers/serviceHelper.js | 457 +- app/docker/helpers/taskHelper.js | 29 +- app/docker/helpers/volumeHelper.js | 49 +- .../interceptors/containersInterceptor.js | 9 +- app/docker/interceptors/imagesInterceptor.js | 9 +- app/docker/interceptors/infoInterceptor.js | 9 +- .../interceptors/networksInterceptor.js | 9 +- app/docker/interceptors/versionInterceptor.js | 9 +- app/docker/interceptors/volumesInterceptor.js | 9 +- app/docker/models/config.js | 11 +- app/docker/models/container.js | 6 +- app/docker/models/containerCapabilities.js | 166 +- app/docker/models/event.js | 268 +- app/docker/models/network.js | 2 +- app/docker/models/node.js | 4 +- app/docker/models/porImageRegistry.js | 8 +- app/docker/models/service.js | 12 +- app/docker/rest/build.js | 45 +- app/docker/rest/commit.js | 25 +- app/docker/rest/config.js | 32 +- app/docker/rest/container.js | 193 +- app/docker/rest/exec.js | 34 +- app/docker/rest/image.js | 95 +- app/docker/rest/network.js | 75 +- app/docker/rest/node.js | 31 +- app/docker/rest/plugin.js | 26 +- app/docker/rest/response/handlers.js | 11 +- app/docker/rest/response/image.js | 9 +- app/docker/rest/secret.js | 32 +- app/docker/rest/service.js | 71 +- app/docker/rest/swarm.js | 25 +- app/docker/rest/system.js | 53 +- app/docker/rest/systemEndpoint.js | 28 +- app/docker/rest/task.js | 39 +- app/docker/rest/volume.js | 40 +- app/docker/services/buildService.js | 110 +- app/docker/services/configService.js | 103 +- app/docker/services/containerService.js | 376 +- app/docker/services/execService.js | 50 +- app/docker/services/imageService.js | 371 +- app/docker/services/networkService.js | 167 +- app/docker/services/nodeService.js | 10 +- app/docker/services/pluginService.js | 118 +- app/docker/services/secretService.js | 103 +- app/docker/services/serviceService.js | 183 +- app/docker/services/swarmService.js | 39 +- app/docker/services/systemService.js | 94 +- app/docker/services/taskService.js | 117 +- app/docker/services/volumeService.js | 192 +- app/docker/views/configs/configs.html | 14 +- app/docker/views/configs/configsController.js | 1 - .../configs/create/createConfigController.js | 43 +- .../views/configs/create/createconfig.html | 12 +- app/docker/views/configs/edit/config.html | 21 +- .../views/configs/edit/configController.js | 54 +- .../views/containers/console/attach.html | 11 +- .../console/containerConsoleController.js | 372 +- app/docker/views/containers/console/exec.html | 49 +- app/docker/views/containers/containers.html | 6 +- .../views/containers/containersController.js | 47 +- .../create/createContainerController.js | 1630 ++-- .../containers/create/createcontainer.html | 153 +- .../views/containers/edit/container.html | 575 +- .../containers/edit/containerController.js | 665 +- .../inspect/containerInspectController.js | 44 +- .../containers/inspect/containerinspect.html | 8 +- .../logs/containerLogsController.js | 142 +- .../views/containers/logs/containerlogs.html | 9 +- .../stats/containerStatsController.js | 277 +- .../containers/stats/containerstats.html | 25 +- app/docker/views/dashboard/dashboard.html | 27 +- .../views/dashboard/dashboardController.js | 102 +- app/docker/views/events/events.html | 7 +- app/docker/views/events/eventsController.js | 36 +- .../host-browser-view-controller.js | 17 +- .../host-browser-view/host-browser-view.html | 6 +- .../host-browser-view/host-browser-view.js | 2 +- .../host/host-job/host-job-controller.js | 17 +- app/docker/views/host/host-job/host-job.js | 2 +- app/docker/views/host/host-view-controller.js | 57 +- app/docker/views/host/host-view.js | 2 +- .../images/build/buildImageController.js | 166 +- app/docker/views/images/build/buildimage.html | 60 +- app/docker/views/images/edit/image.html | 70 +- .../views/images/edit/imageController.js | 281 +- app/docker/views/images/images.html | 44 +- app/docker/views/images/imagesController.js | 240 +- .../images/import/importImageController.js | 60 +- .../views/images/import/importimage.html | 22 +- .../create/createNetworkController.js | 423 +- .../views/networks/create/createnetwork.html | 60 +- app/docker/views/networks/edit/network.html | 16 +- .../views/networks/edit/networkController.js | 186 +- app/docker/views/networks/networks.html | 18 +- .../views/networks/networksController.js | 145 +- .../node-browser/node-browser-controller.js | 21 +- .../nodes/node-browser/node-browser.html | 6 +- .../views/nodes/node-browser/node-browser.js | 2 +- .../node-details-view-controller.js | 38 +- .../nodes/node-details/node-details-view.html | 5 +- .../nodes/node-details/node-details-view.js | 2 +- .../nodes/node-job/node-job-controller.js | 21 +- app/docker/views/nodes/node-job/node-job.html | 6 +- app/docker/views/nodes/node-job/node-job.js | 2 +- .../secrets/create/createSecretController.js | 168 +- .../views/secrets/create/createsecret.html | 26 +- app/docker/views/secrets/edit/secret.html | 10 +- .../views/secrets/edit/secretController.js | 54 +- app/docker/views/secrets/secrets.html | 14 +- app/docker/views/secrets/secretsController.js | 81 +- .../create/createServiceController.js | 1037 +-- .../views/services/create/createservice.html | 92 +- .../services/create/includes/config.html | 4 +- .../create/includes/resources-placement.html | 14 +- .../services/create/includes/secret.html | 14 +- .../create/includes/update-restart.html | 28 +- .../views/services/edit/includes/configs.html | 22 +- .../services/edit/includes/constraints.html | 31 +- .../edit/includes/container-specs.html | 8 +- .../edit/includes/containerlabels.html | 26 +- .../edit/includes/environmentvariables.html | 14 +- .../views/services/edit/includes/hosts.html | 22 +- .../views/services/edit/includes/image.html | 9 +- .../views/services/edit/includes/logging.html | 135 +- .../views/services/edit/includes/mounts.html | 32 +- .../services/edit/includes/networks.html | 2 +- .../edit/includes/placementPreferences.html | 22 +- .../views/services/edit/includes/ports.html | 38 +- .../services/edit/includes/resources.html | 69 +- .../views/services/edit/includes/restart.html | 44 +- .../views/services/edit/includes/secrets.html | 17 +- .../services/edit/includes/servicelabels.html | 20 +- .../views/services/edit/includes/tasks.html | 10 +- .../services/edit/includes/updateconfig.html | 64 +- app/docker/views/services/edit/service.html | 113 +- .../views/services/edit/serviceController.js | 1208 +-- .../services/logs/serviceLogsController.js | 130 +- .../views/services/logs/servicelogs.html | 7 +- app/docker/views/services/services.html | 24 +- .../views/services/servicesController.js | 90 +- app/docker/views/swarm/swarm.html | 16 +- app/docker/views/swarm/swarmController.js | 199 +- .../visualizer/swarmVisualizerController.js | 270 +- .../swarm/visualizer/swarmvisualizer.html | 26 +- app/docker/views/tasks/edit/task.html | 18 +- app/docker/views/tasks/edit/taskController.js | 46 +- .../views/tasks/logs/taskLogsController.js | 142 +- app/docker/views/tasks/logs/tasklogs.html | 10 +- .../volumes/browse/browseVolumeController.js | 24 +- .../views/volumes/browse/browsevolume.html | 7 +- .../volumes/create/createVolumeController.js | 205 +- .../views/volumes/create/createvolume.html | 29 +- app/docker/views/volumes/edit/volume.html | 29 +- .../views/volumes/edit/volumeController.js | 208 +- app/docker/views/volumes/volumes.html | 6 +- app/docker/views/volumes/volumesController.js | 152 +- app/extensions/_module.js | 6 +- app/extensions/oauth/__module.js | 3 +- .../oauth-provider-selector-controller.js | 111 +- .../oauth-providers-selector.html | 10 +- .../oauth-providers-selector.js | 4 +- .../oauth-settings-controller.js | 127 +- .../oauth-settings/oauth-settings.html | 121 +- .../oauth-settings/oauth-settings.js | 4 +- app/extensions/oauth/services/rest/oauth.js | 31 +- app/extensions/rbac/__module.js | 36 +- .../access-viewer/accessViewer.html | 76 +- .../components/access-viewer/accessViewer.js | 2 +- .../access-viewer/accessViewerController.js | 36 +- .../datatable/accessViewerDatatable.html | 145 +- .../datatable/accessViewerDatatable.js | 4 +- .../roles-datatable/rolesDatatable.html | 26 +- .../roles-datatable/rolesDatatable.js | 4 +- .../rbac/directives/authorization.js | 19 +- .../rbac/directives/disable-authorization.js | 17 +- app/extensions/rbac/models/access.js | 4 +- app/extensions/rbac/rest/role.js | 29 +- app/extensions/rbac/services/roleService.js | 70 +- app/extensions/rbac/views/roles/roles.html | 26 +- .../rbac/views/roles/rolesController.js | 1 - app/extensions/registry-management/_module.js | 100 +- .../registryRepositoriesDatatable.html | 32 +- .../registryRepositoriesDatatable.js | 4 +- ...registryRepositoriesDatatableController.js | 36 +- .../registriesRepositoryTagsDatatable.html | 38 +- .../registriesRepositoryTagsDatatable.js | 4 +- ...stryRepositoriesTagsDatatableController.js | 22 +- .../helpers/localRegistryHelper.js | 9 +- .../models/registryImageLayer.js | 2 +- .../models/registryRepository.js | 2 +- .../models/registryTypes.js | 10 +- .../models/repositoryTag.js | 28 +- .../registry-management/rest/catalog.js | 57 +- .../registry-management/rest/gitlab.js | 67 +- .../rest/manifestJquery.js | 153 +- .../registry-management/rest/tags.js | 29 +- .../rest/transform/gitlabResponseGetLink.js | 2 +- .../rest/transform/linkGetResponse.js | 2 +- .../services/registryGitlabService.js | 131 +- .../services/registryServiceSelector.js | 132 +- .../services/registryV2Service.js | 520 +- .../configure/configureRegistryController.js | 121 +- .../views/configure/configureregistry.html | 41 +- .../progression-modal/progressionModal.html | 6 +- .../progression-modal/progressionModal.js | 4 +- .../repositories/edit/registryRepository.html | 63 +- .../edit/registryRepositoryController.js | 761 +- .../repositories/registryRepositories.html | 24 +- .../registryRepositoriesController.js | 116 +- .../tag/registryRepositoryTag.html | 36 +- .../tag/registryRepositoryTagController.js | 19 +- app/index.html | 112 +- app/integrations/_module.js | 4 +- app/integrations/storidge/_module.js | 228 +- .../storidgeClusterEventsDatatable.html | 19 +- .../storidgeClusterEventsDatatable.js | 4 +- .../storidgeDrivesDatatable.html | 39 +- .../storidgeDrivesDatatable.js | 4 +- .../storidgeDrivesDatatableController.js | 76 +- .../storidgeNodesDatatable.html | 25 +- .../nodes-datatable/storidgeNodesDatatable.js | 4 +- .../storidgeNodesDatatableController.js | 102 +- .../storidgeProfileSelector.js | 4 +- .../storidgeProfileSelectorController.js | 32 +- .../storidgeProfilesDatatable.html | 28 +- .../storidgeProfilesDatatable.js | 4 +- .../storidgeSnapshotCreation.html | 14 +- .../storidgeSnapshotCreation.js | 4 +- .../storidgeSnapshotCreationController.js | 50 +- .../storidgeSnapshotsDatatable.html | 27 +- .../storidgeSnapshotsDatatable.js | 4 +- .../volumeStoridgeInfo.html | 32 +- .../volumeStoridgeInfo.js | 4 +- .../volumeStoridgeInfoController.js | 190 +- app/integrations/storidge/filters/filters.js | 100 +- app/integrations/storidge/models/node.js | 2 +- app/integrations/storidge/models/profile.js | 12 +- app/integrations/storidge/models/volume.js | 4 +- app/integrations/storidge/rest/storidge.js | 94 +- .../storidge/services/chartService.js | 348 +- .../storidge/services/clusterService.js | 103 +- .../storidge/services/driveService.js | 128 +- .../storidge/services/nodeService.js | 99 +- .../storidge/services/profileService.js | 95 +- .../storidge/services/snapshotService.js | 137 +- .../storidge/services/volumeService.js | 65 +- .../storidge/views/cluster/cluster.html | 9 +- .../views/cluster/clusterController.js | 158 +- .../storidge/views/drives/drives.html | 20 +- .../storidge/views/drives/drivesController.js | 91 +- .../storidge/views/drives/inspect/drive.html | 6 +- .../views/drives/inspect/driveController.js | 92 +- .../storidge/views/monitor/monitor.html | 19 +- .../views/monitor/monitorController.js | 181 +- .../storidge/views/nodes/inspect/node.html | 14 +- .../views/nodes/inspect/nodeController.js | 208 +- .../create/createProfileController.js | 186 +- .../views/profiles/create/createprofile.html | 67 +- .../storidge/views/profiles/edit/profile.html | 285 +- .../views/profiles/edit/profileController.js | 259 +- .../storidge/views/profiles/profiles.html | 32 +- .../views/profiles/profilesController.js | 128 +- .../views/snapshots/inspect/snapshot.html | 9 +- .../snapshots/inspect/snapshotController.js | 84 +- app/portainer/__module.js | 1112 +-- .../access-datatable/accessDatatable.html | 78 +- .../access-datatable/accessDatatable.js | 4 +- .../accessDatatableController.js | 82 +- .../components/access-table/access-table.js | 10 +- .../components/access-table/accessTable.html | 22 +- .../por-access-control-form.js | 4 +- .../porAccessControlForm.html | 52 +- .../porAccessControlFormController.js | 130 +- .../por-access-control-panel.js | 4 +- .../porAccessControlPanel.html | 74 +- .../porAccessControlPanelController.js | 252 +- .../accessManagement/por-access-management.js | 4 +- .../accessManagement/porAccessManagement.html | 54 +- .../porAccessManagementController.js | 32 +- app/portainer/components/autofocus.js | 26 +- app/portainer/components/buttonSpinner.js | 7 +- .../components/code-editor/code-editor.js | 4 +- .../code-editor/codeEditorController.js | 36 +- .../components/datatables/datatable.css | 33 +- .../endpointsDatatable.html | 36 +- .../endpoints-datatable/endpointsDatatable.js | 4 +- .../endpointsDatatableController.js | 135 +- .../datatables/genericDatatableController.js | 337 +- .../groups-datatable/groupsDatatable.html | 36 +- .../groups-datatable/groupsDatatable.js | 4 +- .../registriesDatatable.html | 47 +- .../registriesDatatable.js | 4 +- .../scheduleTasksDatatable.html | 14 +- .../scheduleTasksDatatable.js | 4 +- .../schedulesDatatable.html | 24 +- .../schedules-datatable/schedulesDatatable.js | 4 +- .../schedulesDatatableController.js | 81 +- .../stacks-datatable/stacksDatatable.html | 45 +- .../stacks-datatable/stacksDatatable.js | 4 +- .../stacksDatatableController.js | 83 +- .../tags-datatable/tagsDatatable.html | 27 +- .../tags-datatable/tagsDatatable.js | 4 +- .../teams-datatable/teamsDatatable.html | 27 +- .../teams-datatable/teamsDatatable.js | 4 +- .../users-datatable/usersDatatable.html | 29 +- .../users-datatable/usersDatatable.js | 4 +- .../endpoint-item/endpoint-item-controller.js | 2 +- .../endpoint-item/endpointItem.html | 37 +- .../endpoint-list/endpoint-list-controller.js | 49 +- .../components/endpoint-list/endpoint-list.js | 6 +- .../endpoint-list/endpointList.html | 27 +- .../endpoint-selector/endpoint-selector.js | 8 +- .../endpointSelectorController.js | 10 +- .../endpointSecurity/por-endpoint-security.js | 4 +- .../endpointSecurity/porEndpointSecurity.html | 17 +- .../porEndpointSecurityController.js | 53 +- .../extension-item/extension-item.js | 4 +- .../extension-item/extensionItemController.js | 22 +- .../extension-list/extension-list.js | 4 +- .../extension-list/extensionList.html | 11 +- .../execute-job-form-controller.js | 125 +- .../execute-job-form/execute-job-form.html | 23 +- .../execute-job-form/execute-job-form.js | 4 +- .../components/forms/group-form/group-form.js | 4 +- .../forms/group-form/groupForm.html | 18 +- .../forms/group-form/groupFormController.js | 39 +- .../registry-form-azure.html | 8 +- .../registry-form-azure.js | 4 +- .../registry-form-custom.html | 16 +- .../registry-form-custom.js | 4 +- .../gitlabProjectsDatatable.html | 17 +- .../gitlabProjectsDatatable.js | 4 +- .../gitlabProjectsDatatableController.js | 90 +- .../registry-form-gitlab.html | 38 +- .../registry-form-gitlab.js | 4 +- .../registry-form-quay.html | 4 +- .../registry-form-quay/registry-form-quay.js | 4 +- .../forms/schedule-form/schedule-form.js | 34 +- .../forms/schedule-form/scheduleForm.html | 90 +- .../forms/template-form/template-form.js | 4 +- .../forms/template-form/templateForm.html | 128 +- .../template-form/templateFormController.js | 87 +- .../group-association-table.js | 40 +- .../groupAssociationTable.html | 19 +- app/portainer/components/header-content.js | 29 +- app/portainer/components/header-title.js | 35 +- app/portainer/components/header.js | 7 +- .../informationPanelOffline.js | 2 +- .../informationPanelOfflineController.js | 62 +- .../information-panel/information-panel.js | 4 +- app/portainer/components/loading.js | 5 +- .../components/motd-panel/motd-panel.js | 4 +- .../components/motd-panel/motdPanel.html | 2 +- .../multiEndpointSelector.html | 2 +- .../multiEndpointSelectorController.js | 4 +- app/portainer/components/onEnterKey.js | 35 +- .../product-list/product-item/product-item.js | 4 +- .../product-item/productItem.html | 6 +- .../components/product-list/product-list.js | 4 +- .../components/product-list/productList.html | 11 +- app/portainer/components/slider/slider.js | 4 +- .../components/slider/sliderController.js | 9 +- .../stack-duplication-form-controller.js | 38 +- .../stack-duplication-form.html | 31 +- .../stack-duplication-form.js | 4 +- .../components/tag-selector/tag-selector.js | 2 +- .../components/tag-selector/tagSelector.html | 11 +- .../template-item/template-item.js | 4 +- .../template-list/template-list-controller.js | 19 +- .../components/template-list/template-list.js | 4 +- .../template-list/templateList.html | 24 +- app/portainer/components/template-widget.js | 7 +- app/portainer/components/tooltip.js | 26 +- app/portainer/components/widget-body.js | 7 +- .../components/widget-custom-header.js | 10 +- app/portainer/components/widget-footer.js | 5 +- app/portainer/components/widget-header.js | 10 +- app/portainer/components/widget-taskbar.js | 7 +- app/portainer/components/widget.js | 7 +- app/portainer/filters/filters.js | 324 +- app/portainer/helpers/endpointHelper.js | 41 +- app/portainer/helpers/formHelper.js | 29 +- .../helpers/resourceControlHelper.js | 3 +- app/portainer/helpers/stackHelper.js | 53 +- app/portainer/helpers/tagHelper.js | 2 +- app/portainer/helpers/templateHelper.js | 224 +- app/portainer/helpers/urlHelper.js | 52 +- app/portainer/helpers/userHelper.js | 27 +- app/portainer/helpers/webhookHelper.js | 27 +- .../interceptors/endpointStatusInterceptor.js | 26 +- app/portainer/models/access.js | 4 +- app/portainer/models/registry.js | 8 +- .../resourceControlOwnership.js | 4 +- .../resourceControlOwnershipParameters.js | 4 +- .../resourceControl/resourceControlTypes.js | 12 +- .../resourceControlUpdatePayload.js | 8 +- app/portainer/models/schedule.js | 2 +- app/portainer/models/settings.js | 2 +- app/portainer/models/status.js | 2 +- app/portainer/models/template.js | 8 +- app/portainer/rest/auth.js | 26 +- app/portainer/rest/dockerhub.js | 23 +- app/portainer/rest/endpoint.js | 45 +- app/portainer/rest/extension.js | 30 +- app/portainer/rest/group.js | 35 +- app/portainer/rest/legacyExtension.js | 27 +- app/portainer/rest/motd.js | 27 +- app/portainer/rest/registry.js | 33 +- app/portainer/rest/resourceControl.js | 27 +- app/portainer/rest/schedule.js | 34 +- app/portainer/rest/settings.js | 27 +- app/portainer/rest/stack.js | 35 +- app/portainer/rest/status.js | 23 +- app/portainer/rest/support.js | 21 +- app/portainer/rest/tag.js | 25 +- app/portainer/rest/team.js | 29 +- app/portainer/rest/teamMembership.js | 27 +- app/portainer/rest/template.js | 27 +- .../rest/transform/getEndpointsTotalCount.js | 2 +- app/portainer/rest/user.js | 37 +- app/portainer/rest/webhooks.js | 26 +- app/portainer/services/api/accessService.js | 170 +- .../services/api/dockerhubService.js | 45 +- app/portainer/services/api/endpointService.js | 221 +- .../services/api/extensionService.js | 159 +- app/portainer/services/api/groupService.js | 88 +- .../services/api/legacyExtensionService.js | 34 +- app/portainer/services/api/motdService.js | 39 +- app/portainer/services/api/registryService.js | 223 +- .../services/api/resourceControlService.js | 285 +- app/portainer/services/api/scheduleService.js | 133 +- app/portainer/services/api/settingsService.js | 77 +- app/portainer/services/api/stackService.js | 566 +- app/portainer/services/api/statusService.js | 65 +- app/portainer/services/api/supportService.js | 37 +- app/portainer/services/api/tagService.js | 101 +- .../services/api/teamMembershipService.js | 81 +- app/portainer/services/api/teamService.js | 147 +- app/portainer/services/api/templateService.js | 216 +- app/portainer/services/api/userService.js | 271 +- app/portainer/services/api/webhookService.js | 57 +- app/portainer/services/async.js | 13 +- app/portainer/services/authentication.js | 195 +- app/portainer/services/chartService.js | 64 +- app/portainer/services/codeMirror.js | 13 +- app/portainer/services/datatableService.js | 119 +- app/portainer/services/endpointProvider.js | 146 +- app/portainer/services/fileUpload.js | 362 +- app/portainer/services/formValidator.js | 38 +- app/portainer/services/httpRequestHelper.js | 103 +- .../services/legacyExtensionManager.js | 145 +- app/portainer/services/localStorage.js | 272 +- app/portainer/services/modalService.js | 423 +- app/portainer/services/notifications.js | 80 +- app/portainer/services/pagination.js | 38 +- app/portainer/services/stateManager.js | 394 +- app/portainer/views/about/about.html | 137 +- app/portainer/views/account/account.html | 25 +- .../views/account/accountController.js | 66 +- app/portainer/views/auth/auth.html | 36 +- app/portainer/views/auth/authController.js | 35 +- .../endpoints/access/endpointAccess.html | 15 +- .../access/endpointAccessController.js | 18 +- .../create/createEndpointController.js | 266 +- .../endpoints/create/createendpoint.html | 142 +- .../views/endpoints/edit/endpoint.html | 130 +- .../endpoints/edit/endpointController.js | 274 +- app/portainer/views/endpoints/endpoints.html | 22 +- .../views/endpoints/endpointsController.js | 86 +- .../views/extensions/extensions.html | 46 +- .../views/extensions/extensionsController.js | 114 +- .../views/extensions/inspect/extension.html | 94 +- .../extensions/inspect/extensionController.js | 156 +- .../views/groups/access/groupAccess.html | 6 +- .../groups/access/groupAccessController.js | 62 +- .../groups/create/createGroupController.js | 54 +- .../views/groups/create/creategroup.html | 6 +- app/portainer/views/groups/edit/group.html | 6 +- .../views/groups/edit/groupController.js | 52 +- app/portainer/views/groups/groups.html | 12 +- .../views/groups/groupsController.js | 74 +- app/portainer/views/home/home.html | 23 +- app/portainer/views/home/homeController.js | 368 +- app/portainer/views/init/admin/initAdmin.html | 27 +- .../views/init/admin/initAdminController.js | 136 +- .../views/init/endpoint/initEndpoint.html | 113 +- .../init/endpoint/initEndpointController.js | 192 +- app/portainer/views/main/mainController.js | 64 +- .../registries/access/registryAccess.html | 3 +- .../access/registryAccessController.js | 60 +- .../create/createRegistryController.js | 182 +- .../registries/create/createregistry.html | 26 +- .../views/registries/edit/registry.html | 20 +- .../registries/edit/registryController.js | 81 +- .../views/registries/registries.html | 35 +- .../views/registries/registriesController.js | 165 +- .../create/createScheduleController.js | 94 +- .../schedules/create/createschedule.html | 4 +- .../views/schedules/edit/schedule.html | 21 +- .../schedules/edit/scheduleController.js | 183 +- app/portainer/views/schedules/schedules.html | 7 +- .../views/schedules/schedulesController.js | 93 +- .../settingsAuthentication.html | 115 +- .../settingsAuthenticationController.js | 293 +- app/portainer/views/settings/settings.html | 68 +- .../views/settings/settingsController.js | 212 +- app/portainer/views/sidebar/sidebar.html | 277 +- .../views/sidebar/sidebarController.js | 74 +- .../stacks/create/createStackController.js | 292 +- .../views/stacks/create/createstack.html | 59 +- app/portainer/views/stacks/edit/stack.html | 64 +- .../views/stacks/edit/stackController.js | 581 +- app/portainer/views/stacks/stacks.html | 16 +- .../views/stacks/stacksController.js | 115 +- .../views/support/product/product.html | 30 +- .../support/product/productController.js | 76 +- app/portainer/views/support/support.html | 9 +- .../views/support/supportController.js | 30 +- app/portainer/views/tags/tags.html | 30 +- app/portainer/views/tags/tagsController.js | 127 +- app/portainer/views/teams/edit/team.html | 34 +- .../views/teams/edit/teamController.js | 373 +- app/portainer/views/teams/teams.html | 61 +- app/portainer/views/teams/teamsController.js | 189 +- .../create/createTemplateController.js | 92 +- .../templates/create/createtemplate.html | 4 +- .../views/templates/edit/template.html | 4 +- .../templates/edit/templateController.js | 111 +- app/portainer/views/templates/templates.html | 81 +- .../views/templates/templatesController.js | 535 +- .../views/update-password/updatePassword.html | 115 +- .../updatePasswordController.js | 70 +- app/portainer/views/users/edit/user.html | 30 +- .../views/users/edit/userController.js | 147 +- app/portainer/views/users/users.html | 82 +- app/portainer/views/users/usersController.js | 241 +- app/vendors.js | 2 +- assets/css/app.css | 149 +- gruntfile.js | 138 +- package.json | 18 +- postcss.config.js | 4 +- test/e2e/cypress/integration/spec.js | 29 +- test/e2e/cypress/plugins/index.js | 2 +- test/e2e/cypress/support/index.js | 2 +- .../components/containerController.spec.js | 102 +- .../components/containerTopController.spec.js | 58 +- .../app/components/networkController.spec.js | 158 +- .../app/components/networksController.spec.js | 200 +- .../startContainerController.spec.js | 452 +- .../app/components/statsController.spec.js | 56 +- .../app/components/volumesController.spec.js | 115 +- test/unit/app/shared/filters.spec.js | 185 +- test/unit/karma.conf.js | 10 +- webpack/webpack.common.js | 54 +- webpack/webpack.develop.js | 14 +- webpack/webpack.production.js | 18 +- yarn.lock | 7233 ++++++++--------- 714 files changed, 31228 insertions(+), 28305 deletions(-) create mode 100644 .prettierrc diff --git a/.gitignore b/.gitignore index 43b170ab0..d9c64e8bc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ dist portainer-checksum.txt api/cmd/portainer/portainer* .tmp -.vscode \ No newline at end of file +.vscode +.eslintcache \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..3217d14ee --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "printWidth": 180, + "singleQuote": true, + "htmlWhitespaceSensitivity": "strict", + "overrides": [ + { + "files": ["*.html"], + "options": { + "parser": "angular" + } + } + ] +} diff --git a/app/__module.js b/app/__module.js index 881453c9a..e5db84c05 100644 --- a/app/__module.js +++ b/app/__module.js @@ -32,12 +32,12 @@ angular.module('portainer', [ 'portainer.extensions', 'portainer.integrations', 'rzModule', - 'moment-picker' + 'moment-picker', ]); if (require) { var req = require.context('./', true, /^(.*\.(js$))[^.]*$/im); - req.keys().forEach(function(key) { + req.keys().forEach(function (key) { req(key); }); } diff --git a/app/agent/components/file-uploader/file-uploader-controller.js b/app/agent/components/file-uploader/file-uploader-controller.js index e6516c67c..d0c5ad798 100644 --- a/app/agent/components/file-uploader/file-uploader-controller.js +++ b/app/agent/components/file-uploader/file-uploader-controller.js @@ -4,7 +4,7 @@ angular.module('portainer.agent').controller('FileUploaderController', [ var ctrl = this; ctrl.state = { - uploadInProgress: false + uploadInProgress: false, }; ctrl.onFileSelected = onFileSelected; @@ -19,5 +19,5 @@ angular.module('portainer.agent').controller('FileUploaderController', [ ctrl.state.uploadInProgress = false; }); } - } + }, ]); diff --git a/app/agent/components/file-uploader/file-uploader.html b/app/agent/components/file-uploader/file-uploader.html index e092ce5d6..5354b5c2f 100644 --- a/app/agent/components/file-uploader/file-uploader.html +++ b/app/agent/components/file-uploader/file-uploader.html @@ -1,6 +1,3 @@ - diff --git a/app/agent/components/file-uploader/file-uploader.js b/app/agent/components/file-uploader/file-uploader.js index 6427bae4a..6232c7f8d 100644 --- a/app/agent/components/file-uploader/file-uploader.js +++ b/app/agent/components/file-uploader/file-uploader.js @@ -2,6 +2,6 @@ angular.module('portainer.agent').component('fileUploader', { templateUrl: './file-uploader.html', controller: 'FileUploaderController', bindings: { - uploadFile: ' - - - + +
@@ -43,36 +49,33 @@ - - + + - - + + @@ -28,4 +28,4 @@ - \ No newline at end of file + diff --git a/app/docker/components/host-view-panels/devices-panel/devices-panel.js b/app/docker/components/host-view-panels/devices-panel/devices-panel.js index 8049add85..a7d19bcae 100644 --- a/app/docker/components/host-view-panels/devices-panel/devices-panel.js +++ b/app/docker/components/host-view-panels/devices-panel/devices-panel.js @@ -1,6 +1,6 @@ angular.module('portainer.docker').component('devicesPanel', { templateUrl: './devices-panel.html', bindings: { - devices: '<' - } + devices: '<', + }, }); diff --git a/app/docker/components/host-view-panels/disks-panel/disks-panel.html b/app/docker/components/host-view-panels/disks-panel/disks-panel.html index 632f07151..f2f59ec2c 100644 --- a/app/docker/components/host-view-panels/disks-panel/disks-panel.html +++ b/app/docker/components/host-view-panels/disks-panel/disks-panel.html @@ -12,8 +12,8 @@ - - + + @@ -28,4 +28,4 @@ - \ No newline at end of file + diff --git a/app/docker/components/host-view-panels/disks-panel/disks-panel.js b/app/docker/components/host-view-panels/disks-panel/disks-panel.js index 3997a89b3..4b755a607 100644 --- a/app/docker/components/host-view-panels/disks-panel/disks-panel.js +++ b/app/docker/components/host-view-panels/disks-panel/disks-panel.js @@ -1,6 +1,6 @@ angular.module('portainer.docker').component('disksPanel', { templateUrl: './disks-panel.html', bindings: { - disks: '<' - } + disks: '<', + }, }); diff --git a/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.html b/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.html index 036e4d7b7..5b25d0ea2 100644 --- a/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.html +++ b/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.html @@ -7,7 +7,9 @@ - + @@ -31,11 +33,11 @@ - +
- Go - to parent + Go to parent
- + - + - {{ item.Name }} - - - {{ - item.Name }} + {{ item.Name }} + {{ item.Name }} {{ item.Size | humansize }} {{ item.ModTime | getisodatefromtimestamp }} - + Download @@ -94,4 +97,4 @@ - \ No newline at end of file + diff --git a/app/agent/components/files-datatable/files-datatable.js b/app/agent/components/files-datatable/files-datatable.js index d1735cf48..d75d0f970 100644 --- a/app/agent/components/files-datatable/files-datatable.js +++ b/app/agent/components/files-datatable/files-datatable.js @@ -15,8 +15,8 @@ angular.module('portainer.agent').component('filesDatatable', { rename: '&', download: '&', delete: '&', - + isUploadAllowed: '<', - onFileSelectedForUpload: '<' - } + onFileSelectedForUpload: '<', + }, }); diff --git a/app/agent/components/host-browser/host-browser-controller.js b/app/agent/components/host-browser/host-browser-controller.js index 797f5c6ac..c9c1e5e0f 100644 --- a/app/agent/components/host-browser/host-browser-controller.js +++ b/app/agent/components/host-browser/host-browser-controller.js @@ -1,12 +1,15 @@ import _ from 'lodash-es'; angular.module('portainer.agent').controller('HostBrowserController', [ - 'HostBrowserService', 'Notifications', 'FileSaver', 'ModalService', + 'HostBrowserService', + 'Notifications', + 'FileSaver', + 'ModalService', function HostBrowserController(HostBrowserService, Notifications, FileSaver, ModalService) { var ctrl = this; var ROOT_PATH = '/host'; ctrl.state = { - path: ROOT_PATH + path: ROOT_PATH, }; ctrl.goToParent = goToParent; @@ -21,7 +24,7 @@ angular.module('portainer.agent').controller('HostBrowserController', [ function getRelativePath(path) { path = path || ctrl.state.path; - var rootPathRegex = new RegExp('^' + ROOT_PATH + '\/?'); + var rootPathRegex = new RegExp('^' + ROOT_PATH + '/?'); var relativePath = path.replace(rootPathRegex, '/'); return relativePath; } @@ -71,7 +74,7 @@ angular.module('portainer.agent').controller('HostBrowserController', [ HostBrowserService.get(filePath) .then(function onFileReceived(data) { var downloadData = new Blob([data.file], { - type: 'text/plain;charset=utf-8' + type: 'text/plain;charset=utf-8', }); FileSaver.saveAs(downloadData, file); }) @@ -83,15 +86,12 @@ angular.module('portainer.agent').controller('HostBrowserController', [ function confirmDeleteFile(name) { var filePath = buildPath(ctrl.state.path, name); - ModalService.confirmDeletion( - 'Are you sure that you want to delete ' + getRelativePath(filePath) + ' ?', - function onConfirm(confirmed) { - if (!confirmed) { - return; - } - return deleteFile(filePath); + ModalService.confirmDeletion('Are you sure that you want to delete ' + getRelativePath(filePath) + ' ?', function onConfirm(confirmed) { + if (!confirmed) { + return; } - ); + return deleteFile(filePath); + }); } function deleteFile(path) { @@ -145,5 +145,5 @@ angular.module('portainer.agent').controller('HostBrowserController', [ function refreshList() { getFilesForPath(ctrl.state.path); } - } + }, ]); diff --git a/app/agent/components/host-browser/host-browser.html b/app/agent/components/host-browser/host-browser.html index cf7b59306..b81ecc744 100644 --- a/app/agent/components/host-browser/host-browser.html +++ b/app/agent/components/host-browser/host-browser.html @@ -1,6 +1,8 @@ - diff --git a/app/agent/components/host-browser/host-browser.js b/app/agent/components/host-browser/host-browser.js index d702255eb..9b16b5a01 100644 --- a/app/agent/components/host-browser/host-browser.js +++ b/app/agent/components/host-browser/host-browser.js @@ -1,5 +1,5 @@ angular.module('portainer.agent').component('hostBrowser', { controller: 'HostBrowserController', templateUrl: './host-browser.html', - bindings: {} + bindings: {}, }); diff --git a/app/agent/components/node-selector/node-selector.js b/app/agent/components/node-selector/node-selector.js index 0d9c93ed3..2f7e14e82 100644 --- a/app/agent/components/node-selector/node-selector.js +++ b/app/agent/components/node-selector/node-selector.js @@ -2,6 +2,6 @@ angular.module('portainer.agent').component('nodeSelector', { templateUrl: './nodeSelector.html', controller: 'NodeSelectorController', bindings: { - model: '=' - } + model: '=', + }, }); diff --git a/app/agent/components/node-selector/nodeSelector.html b/app/agent/components/node-selector/nodeSelector.html index 1a910396c..4ab9e75bb 100644 --- a/app/agent/components/node-selector/nodeSelector.html +++ b/app/agent/components/node-selector/nodeSelector.html @@ -1,8 +1,6 @@
- +
diff --git a/app/agent/components/node-selector/nodeSelectorController.js b/app/agent/components/node-selector/nodeSelectorController.js index be2f6ce92..ce44f8568 100644 --- a/app/agent/components/node-selector/nodeSelectorController.js +++ b/app/agent/components/node-selector/nodeSelectorController.js @@ -1,18 +1,20 @@ -angular.module('portainer.agent') -.controller('NodeSelectorController', ['AgentService', 'Notifications', function (AgentService, Notifications) { - var ctrl = this; +angular.module('portainer.agent').controller('NodeSelectorController', [ + 'AgentService', + 'Notifications', + function (AgentService, Notifications) { + var ctrl = this; - this.$onInit = function() { - AgentService.agents() - .then(function success(data) { - ctrl.agents = data; - if (!ctrl.model) { - ctrl.model = data[0].NodeName; - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to load agents'); - }); - }; - -}]); + this.$onInit = function () { + AgentService.agents() + .then(function success(data) { + ctrl.agents = data; + if (!ctrl.model) { + ctrl.model = data[0].NodeName; + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to load agents'); + }); + }; + }, +]); diff --git a/app/agent/components/volume-browser/volume-browser.js b/app/agent/components/volume-browser/volume-browser.js index bea11e61a..5c2b2b78d 100644 --- a/app/agent/components/volume-browser/volume-browser.js +++ b/app/agent/components/volume-browser/volume-browser.js @@ -4,6 +4,6 @@ angular.module('portainer.agent').component('volumeBrowser', { bindings: { volumeId: '<', nodeName: '<', - isUploadEnabled: '<' - } + isUploadEnabled: '<', + }, }); diff --git a/app/agent/components/volume-browser/volumeBrowser.html b/app/agent/components/volume-browser/volumeBrowser.html index 97c8a4da6..5b85cfd94 100644 --- a/app/agent/components/volume-browser/volumeBrowser.html +++ b/app/agent/components/volume-browser/volumeBrowser.html @@ -1,6 +1,8 @@ diff --git a/app/agent/components/volume-browser/volumeBrowserController.js b/app/agent/components/volume-browser/volumeBrowserController.js index 0f8e99d86..fccde7753 100644 --- a/app/agent/components/volume-browser/volumeBrowserController.js +++ b/app/agent/components/volume-browser/volumeBrowserController.js @@ -1,137 +1,137 @@ import _ from 'lodash-es'; -angular.module('portainer.agent') -.controller('VolumeBrowserController', ['HttpRequestHelper', 'VolumeBrowserService', 'FileSaver', 'Blob', 'ModalService', 'Notifications', -function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) { - var ctrl = this; +angular.module('portainer.agent').controller('VolumeBrowserController', [ + 'HttpRequestHelper', + 'VolumeBrowserService', + 'FileSaver', + 'Blob', + 'ModalService', + 'Notifications', + function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) { + var ctrl = this; - this.state = { - path: '/' - }; + this.state = { + path: '/', + }; - this.rename = function(file, newName) { - var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; - var newFilePath = this.state.path === '/' ? newName : this.state.path + '/' + newName; + this.rename = function (file, newName) { + var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; + var newFilePath = this.state.path === '/' ? newName : this.state.path + '/' + newName; - VolumeBrowserService.rename(this.volumeId, filePath, newFilePath) - .then(function success() { - Notifications.success('File successfully renamed', newFilePath); - return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path); - }) - .then(function success(data) { - ctrl.files = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to rename file'); - }); - }; + VolumeBrowserService.rename(this.volumeId, filePath, newFilePath) + .then(function success() { + Notifications.success('File successfully renamed', newFilePath); + return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path); + }) + .then(function success(data) { + ctrl.files = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to rename file'); + }); + }; - this.delete = function(file) { - var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; + this.delete = function (file) { + var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; - ModalService.confirmDeletion( - 'Are you sure that you want to delete ' + filePath + ' ?', - function onConfirm(confirmed) { - if(!confirmed) { return; } + ModalService.confirmDeletion('Are you sure that you want to delete ' + filePath + ' ?', function onConfirm(confirmed) { + if (!confirmed) { + return; + } deleteFile(filePath); - } - ); - }; - - this.download = function(file) { - var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; - VolumeBrowserService.get(this.volumeId, filePath) - .then(function success(data) { - var downloadData = new Blob([data.file]); - FileSaver.saveAs(downloadData, file); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to download file'); - }); - }; - - this.up = function() { - var parentFolder = parentPath(this.state.path); - browse(parentFolder); - }; - - this.browse = function(folder) { - var path = buildPath(this.state.path, folder); - browse(path); - }; - - function deleteFile(file) { - VolumeBrowserService.delete(ctrl.volumeId, file) - .then(function success() { - Notifications.success('File successfully deleted', file); - return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path); - }) - .then(function success(data) { - ctrl.files = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to delete file'); - }); - } - - - function browse(path) { - VolumeBrowserService.ls(ctrl.volumeId, path) - .then(function success(data) { - ctrl.state.path = path; - ctrl.files = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to browse volume'); - }); - } - - this.onFileSelectedForUpload = function onFileSelectedForUpload(file) { - VolumeBrowserService.upload(ctrl.state.path, file, ctrl.volumeId) - .then(function onFileUpload() { - onFileUploaded(); - }) - .catch(function onFileUpload(err) { - Notifications.error('Failure', err, 'Unable to upload file'); }); - }; + }; - function parentPath(path) { - if (path.lastIndexOf('/') === 0) { - return '/'; + this.download = function (file) { + var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; + VolumeBrowserService.get(this.volumeId, filePath) + .then(function success(data) { + var downloadData = new Blob([data.file]); + FileSaver.saveAs(downloadData, file); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to download file'); + }); + }; + + this.up = function () { + var parentFolder = parentPath(this.state.path); + browse(parentFolder); + }; + + this.browse = function (folder) { + var path = buildPath(this.state.path, folder); + browse(path); + }; + + function deleteFile(file) { + VolumeBrowserService.delete(ctrl.volumeId, file) + .then(function success() { + Notifications.success('File successfully deleted', file); + return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path); + }) + .then(function success(data) { + ctrl.files = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to delete file'); + }); } - var split = _.split(path, '/'); - return _.join(_.slice(split, 0, split.length - 1), '/'); - } - - function buildPath(parent, file) { - if (parent === '/') { - return parent + file; + function browse(path) { + VolumeBrowserService.ls(ctrl.volumeId, path) + .then(function success(data) { + ctrl.state.path = path; + ctrl.files = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to browse volume'); + }); } - return parent + '/' + file; - } + this.onFileSelectedForUpload = function onFileSelectedForUpload(file) { + VolumeBrowserService.upload(ctrl.state.path, file, ctrl.volumeId) + .then(function onFileUpload() { + onFileUploaded(); + }) + .catch(function onFileUpload(err) { + Notifications.error('Failure', err, 'Unable to upload file'); + }); + }; - this.$onInit = function() { - HttpRequestHelper.setPortainerAgentTargetHeader(this.nodeName); - VolumeBrowserService.ls(this.volumeId, this.state.path) - .then(function success(data) { - ctrl.files = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to browse volume'); - }); - }; + function parentPath(path) { + if (path.lastIndexOf('/') === 0) { + return '/'; + } - function onFileUploaded() { - refreshList(); - } + var split = _.split(path, '/'); + return _.join(_.slice(split, 0, split.length - 1), '/'); + } - function refreshList() { - browse(ctrl.state.path); - } + function buildPath(parent, file) { + if (parent === '/') { + return parent + file; + } + return parent + '/' + file; + } - + this.$onInit = function () { + HttpRequestHelper.setPortainerAgentTargetHeader(this.nodeName); + VolumeBrowserService.ls(this.volumeId, this.state.path) + .then(function success(data) { + ctrl.files = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to browse volume'); + }); + }; -}]); + function onFileUploaded() { + refreshList(); + } + + function refreshList() { + browse(ctrl.state.path); + } + }, +]); diff --git a/app/agent/models/agent.js b/app/agent/models/agent.js index 3e171ff07..54866ef90 100644 --- a/app/agent/models/agent.js +++ b/app/agent/models/agent.js @@ -1,5 +1,5 @@ export function AgentViewModel(data) { this.IPAddress = data.IPAddress; - this.NodeName = data.NodeName; - this.NodeRole = data.NodeRole; + this.NodeName = data.NodeName; + this.NodeRole = data.NodeRole; } diff --git a/app/agent/rest/agent.js b/app/agent/rest/agent.js index 522296c3a..d7d0c6a2f 100644 --- a/app/agent/rest/agent.js +++ b/app/agent/rest/agent.js @@ -1,12 +1,19 @@ -angular.module('portainer.agent') -.factory('Agent', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'StateManager', +angular.module('portainer.agent').factory('Agent', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + 'StateManager', function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/agents', { - endpointId: EndpointProvider.endpointID, - version: StateManager.getAgentApiVersion + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/agents', + { + endpointId: EndpointProvider.endpointID, + version: StateManager.getAgentApiVersion, + }, + { + query: { method: 'GET', isArray: true }, + } + ); }, - { - query: { method: 'GET', isArray: true } - }); -}]); +]); diff --git a/app/agent/rest/browse.js b/app/agent/rest/browse.js index 82e5d4b21..852ee0535 100644 --- a/app/agent/rest/browse.js +++ b/app/agent/rest/browse.js @@ -1,27 +1,39 @@ import { browseGetResponse } from './response/browse'; -angular.module('portainer.agent') -.factory('Browse', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'StateManager', +angular.module('portainer.agent').factory('Browse', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + 'StateManager', function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/browse/:action', { - endpointId: EndpointProvider.endpointID, - version: StateManager.getAgentApiVersion + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/browse/:action', + { + endpointId: EndpointProvider.endpointID, + version: StateManager.getAgentApiVersion, + }, + { + ls: { + method: 'GET', + isArray: true, + params: { action: 'ls' }, + }, + get: { + method: 'GET', + params: { action: 'get' }, + transformResponse: browseGetResponse, + responseType: 'arraybuffer', + }, + delete: { + method: 'DELETE', + params: { action: 'delete' }, + }, + rename: { + method: 'PUT', + params: { action: 'rename' }, + }, + } + ); }, - { - ls: { - method: 'GET', isArray: true, params: { action: 'ls' } - }, - get: { - method: 'GET', params: { action: 'get' }, - transformResponse: browseGetResponse, - responseType: 'arraybuffer' - }, - delete: { - method: 'DELETE', params: { action: 'delete' } - }, - rename: { - method: 'PUT', params: { action: 'rename' } - } - }); -}]); +]); diff --git a/app/agent/rest/host.js b/app/agent/rest/host.js index f184d2544..a5fb198ae 100644 --- a/app/agent/rest/host.js +++ b/app/agent/rest/host.js @@ -1,16 +1,19 @@ angular.module('portainer.agent').factory('Host', [ - '$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'StateManager', + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + 'StateManager', function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { 'use strict'; return $resource( API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/host/:action', { endpointId: EndpointProvider.endpointID, - version: StateManager.getAgentApiVersion + version: StateManager.getAgentApiVersion, }, { - info: { method: 'GET', params: { action: 'info' } } + info: { method: 'GET', params: { action: 'info' } }, } ); - } + }, ]); diff --git a/app/agent/rest/ping.js b/app/agent/rest/ping.js index 7eeb93f2e..b6527dfb4 100644 --- a/app/agent/rest/ping.js +++ b/app/agent/rest/ping.js @@ -1,11 +1,14 @@ angular.module('portainer.agent').factory('AgentPing', [ - '$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', '$q', + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + '$q', function AgentPingFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q) { 'use strict'; return $resource( API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/ping', - { - endpointId: EndpointProvider.endpointID + { + endpointId: EndpointProvider.endpointID, }, { ping: { @@ -13,8 +16,7 @@ angular.module('portainer.agent').factory('AgentPing', [ interceptor: { response: function versionInterceptor(response) { var instance = response.resource; - var version = - response.headers('Portainer-Agent-Api-Version') || 1; + var version = response.headers('Portainer-Agent-Api-Version') || 1; instance.version = Number(version); return instance; }, @@ -24,10 +26,10 @@ angular.module('portainer.agent').factory('AgentPing', [ return { version: 1 }; } return $q.reject(error); - } - } - } + }, + }, + }, } ); - } + }, ]); diff --git a/app/agent/rest/v1/agent.js b/app/agent/rest/v1/agent.js index a78755b35..3d9e8d606 100644 --- a/app/agent/rest/v1/agent.js +++ b/app/agent/rest/v1/agent.js @@ -1,10 +1,17 @@ -angular.module('portainer.agent') -.factory('AgentVersion1', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/agents', { - endpointId: EndpointProvider.endpointID +angular.module('portainer.agent').factory('AgentVersion1', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/agents', + { + endpointId: EndpointProvider.endpointID, + }, + { + query: { method: 'GET', isArray: true }, + } + ); }, - { - query: { method: 'GET', isArray: true } - }); -}]); +]); diff --git a/app/agent/rest/v1/browse.js b/app/agent/rest/v1/browse.js index 62a198743..89c18b384 100644 --- a/app/agent/rest/v1/browse.js +++ b/app/agent/rest/v1/browse.js @@ -1,25 +1,37 @@ import { browseGetResponse } from '../response/browse'; -angular.module('portainer.agent') -.factory('BrowseVersion1', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:volumeID/:action', { - endpointId: EndpointProvider.endpointID +angular.module('portainer.agent').factory('BrowseVersion1', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:volumeID/:action', + { + endpointId: EndpointProvider.endpointID, + }, + { + ls: { + method: 'GET', + isArray: true, + params: { action: 'ls' }, + }, + get: { + method: 'GET', + params: { action: 'get' }, + transformResponse: browseGetResponse, + responseType: 'arraybuffer', + }, + delete: { + method: 'DELETE', + params: { action: 'delete' }, + }, + rename: { + method: 'PUT', + params: { action: 'rename' }, + }, + } + ); }, - { - ls: { - method: 'GET', isArray: true, params: { action: 'ls' } - }, - get: { - method: 'GET', params: { action: 'get' }, - transformResponse: browseGetResponse, - responseType: 'arraybuffer' - }, - delete: { - method: 'DELETE', params: { action: 'delete' } - }, - rename: { - method: 'PUT', params: { action: 'rename' } - } - }); -}]); +]); diff --git a/app/agent/services/agentService.js b/app/agent/services/agentService.js index af471da09..b9d938d94 100644 --- a/app/agent/services/agentService.js +++ b/app/agent/services/agentService.js @@ -1,7 +1,12 @@ import { AgentViewModel } from '../models/agent'; angular.module('portainer.agent').factory('AgentService', [ - '$q', 'Agent', 'AgentVersion1', 'HttpRequestHelper', 'Host', 'StateManager', + '$q', + 'Agent', + 'AgentVersion1', + 'HttpRequestHelper', + 'Host', + 'StateManager', function AgentServiceFactory($q, Agent, AgentVersion1, HttpRequestHelper, Host, StateManager) { 'use strict'; var service = {}; @@ -24,10 +29,11 @@ angular.module('portainer.agent').factory('AgentService', [ var agentVersion = getAgentApiVersion(); var service = agentVersion > 1 ? Agent : AgentVersion1; - - service.query({ version: agentVersion }) + + service + .query({ version: agentVersion }) .$promise.then(function success(data) { - var agents = data.map(function(item) { + var agents = data.map(function (item) { return new AgentViewModel(item); }); deferred.resolve(agents); @@ -40,5 +46,5 @@ angular.module('portainer.agent').factory('AgentService', [ } return service; - } + }, ]); diff --git a/app/agent/services/hostBrowserService.js b/app/agent/services/hostBrowserService.js index 6f292c36a..8de01f815 100644 --- a/app/agent/services/hostBrowserService.js +++ b/app/agent/services/hostBrowserService.js @@ -1,5 +1,10 @@ angular.module('portainer.agent').factory('HostBrowserService', [ - 'Browse', 'Upload', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', '$q', 'StateManager', + 'Browse', + 'Upload', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + '$q', + 'StateManager', function HostBrowserServiceFactory(Browse, Upload, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q, StateManager) { var service = {}; @@ -24,7 +29,7 @@ angular.module('portainer.agent').factory('HostBrowserService', [ function rename(path, newPath) { var payload = { CurrentFilePath: path, - NewFilePath: newPath + NewFilePath: newPath, }; return Browse.rename({}, payload).$promise; } @@ -32,21 +37,15 @@ angular.module('portainer.agent').factory('HostBrowserService', [ function upload(path, file, onProgress) { var deferred = $q.defer(); var agentVersion = StateManager.getAgentApiVersion(); - var url = - API_ENDPOINT_ENDPOINTS + - '/' + - EndpointProvider.endpointID() + - '/docker' + - (agentVersion > 1 ? '/v' + agentVersion : '') + - '/browse/put'; + var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker' + (agentVersion > 1 ? '/v' + agentVersion : '') + '/browse/put'; Upload.upload({ url: url, - data: { file: file, Path: path } + data: { file: file, Path: path }, }).then(deferred.resolve, deferred.reject, onProgress); return deferred.promise; } return service; - } + }, ]); diff --git a/app/agent/services/pingService.js b/app/agent/services/pingService.js index 765d47a5f..cc3133f22 100644 --- a/app/agent/services/pingService.js +++ b/app/agent/services/pingService.js @@ -10,5 +10,5 @@ angular.module('portainer.agent').service('AgentPingService', [ } return service; - } + }, ]); diff --git a/app/agent/services/volumeBrowserService.js b/app/agent/services/volumeBrowserService.js index a4ffdbf9d..002edb08b 100644 --- a/app/agent/services/volumeBrowserService.js +++ b/app/agent/services/volumeBrowserService.js @@ -1,5 +1,11 @@ angular.module('portainer.agent').factory('VolumeBrowserService', [ - 'StateManager', 'Browse', 'BrowseVersion1', '$q', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'Upload', + 'StateManager', + 'Browse', + 'BrowseVersion1', + '$q', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + 'Upload', function VolumeBrowserServiceFactory(StateManager, Browse, BrowseVersion1, $q, API_ENDPOINT_ENDPOINTS, EndpointProvider, Upload) { 'use strict'; var service = {}; @@ -14,22 +20,22 @@ angular.module('portainer.agent').factory('VolumeBrowserService', [ return agentVersion > 1 ? Browse : BrowseVersion1; } - service.ls = function(volumeId, path) { + service.ls = function (volumeId, path) { return getBrowseService().ls({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise; }; - service.get = function(volumeId, path) { + service.get = function (volumeId, path) { return getBrowseService().get({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise; }; - service.delete = function(volumeId, path) { + service.delete = function (volumeId, path) { return getBrowseService().delete({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise; }; - service.rename = function(volumeId, path, newPath) { + service.rename = function (volumeId, path, newPath) { var payload = { - CurrentFilePath: path, - NewFilePath: newPath + CurrentFilePath: path, + NewFilePath: newPath, }; return getBrowseService().rename({ volumeID: volumeId, version: getAgentApiVersion() }, payload).$promise; }; @@ -37,26 +43,19 @@ angular.module('portainer.agent').factory('VolumeBrowserService', [ service.upload = function upload(path, file, volumeId, onProgress) { var deferred = $q.defer(); var agentVersion = StateManager.getAgentApiVersion(); - if (agentVersion <2) { + if (agentVersion < 2) { deferred.reject('upload is not supported on this agent version'); return; } - var url = - API_ENDPOINT_ENDPOINTS + - '/' + - EndpointProvider.endpointID() + - '/docker' + - '/v' + agentVersion + - '/browse/put?volumeID=' + - volumeId; + var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker' + '/v' + agentVersion + '/browse/put?volumeID=' + volumeId; Upload.upload({ url: url, - data: { file: file, Path: path } + data: { file: file, Path: path }, }).then(deferred.resolve, deferred.reject, onProgress); return deferred.promise; }; return service; - } + }, ]); diff --git a/app/app.js b/app/app.js index 95cf796bf..6cec23d30 100644 --- a/app/app.js +++ b/app/app.js @@ -1,46 +1,55 @@ import $ from 'jquery'; -import '@babel/polyfill' +import '@babel/polyfill'; -angular.module('portainer') -.run(['$rootScope', '$state', '$interval', 'LocalStorage', 'EndpointProvider', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper', -function ($rootScope, $state, $interval, LocalStorage, EndpointProvider, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) { - 'use strict'; +angular.module('portainer').run([ + '$rootScope', + '$state', + '$interval', + 'LocalStorage', + 'EndpointProvider', + 'SystemService', + 'cfpLoadingBar', + '$transitions', + 'HttpRequestHelper', + function ($rootScope, $state, $interval, LocalStorage, EndpointProvider, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) { + 'use strict'; - EndpointProvider.initialize(); + EndpointProvider.initialize(); - $rootScope.$state = $state; + $rootScope.$state = $state; - // Workaround to prevent the loading bar from going backward - // https://github.com/chieffancypants/angular-loading-bar/issues/273 - var originalSet = cfpLoadingBar.set; - cfpLoadingBar.set = function overrideSet(n) { - if (n > cfpLoadingBar.status()) { - originalSet.apply(cfpLoadingBar, arguments); - } - }; + // Workaround to prevent the loading bar from going backward + // https://github.com/chieffancypants/angular-loading-bar/issues/273 + var originalSet = cfpLoadingBar.set; + cfpLoadingBar.set = function overrideSet(n) { + if (n > cfpLoadingBar.status()) { + originalSet.apply(cfpLoadingBar, arguments); + } + }; - $transitions.onBefore({}, function() { - HttpRequestHelper.resetAgentHeaders(); - }); + $transitions.onBefore({}, function () { + HttpRequestHelper.resetAgentHeaders(); + }); - $state.defaultErrorHandler(function() { - // Do not log transitionTo errors - }); + $state.defaultErrorHandler(function () { + // Do not log transitionTo errors + }); - // Keep-alive Edge endpoints by sending a ping request every minute - $interval(function() { - ping(EndpointProvider, SystemService); - }, 60 * 1000) + // Keep-alive Edge endpoints by sending a ping request every minute + $interval(function () { + ping(EndpointProvider, SystemService); + }, 60 * 1000); - $(document).ajaxSend(function (event, jqXhr, jqOpts) { - const type = jqOpts.type === 'POST' || jqOpts.type === 'PUT' || jqOpts.type === 'PATCH'; - const hasNoContentType = jqOpts.contentType !== 'application/json' && jqOpts.headers && !jqOpts.headers['Content-Type']; - if (type && hasNoContentType) { - jqXhr.setRequestHeader('Content-Type', 'application/json'); - } - jqXhr.setRequestHeader('Authorization', 'Bearer ' + LocalStorage.getJWT()); - }); -}]); + $(document).ajaxSend(function (event, jqXhr, jqOpts) { + const type = jqOpts.type === 'POST' || jqOpts.type === 'PUT' || jqOpts.type === 'PATCH'; + const hasNoContentType = jqOpts.contentType !== 'application/json' && jqOpts.headers && !jqOpts.headers['Content-Type']; + if (type && hasNoContentType) { + jqXhr.setRequestHeader('Content-Type', 'application/json'); + } + jqXhr.setRequestHeader('Authorization', 'Bearer ' + LocalStorage.getJWT()); + }); + }, +]); function ping(EndpointProvider, SystemService) { let endpoint = EndpointProvider.currentEndpoint(); diff --git a/app/azure/_module.js b/app/azure/_module.js index 663f2c511..a11a5aa5e 100644 --- a/app/azure/_module.js +++ b/app/azure/_module.js @@ -1,49 +1,51 @@ -angular.module('portainer.azure', ['portainer.app']) -.config(['$stateRegistryProvider', function ($stateRegistryProvider) { - 'use strict'; +angular.module('portainer.azure', ['portainer.app']).config([ + '$stateRegistryProvider', + function ($stateRegistryProvider) { + 'use strict'; - var azure = { - name: 'azure', - url: '/azure', - parent: 'root', - abstract: true - }; + var azure = { + name: 'azure', + url: '/azure', + parent: 'root', + abstract: true, + }; - var containerInstances = { - name: 'azure.containerinstances', - url: '/containerinstances', - views: { - 'content@': { - templateUrl: './views/containerinstances/containerinstances.html', - controller: 'AzureContainerInstancesController' - } - } - }; + var containerInstances = { + name: 'azure.containerinstances', + url: '/containerinstances', + views: { + 'content@': { + templateUrl: './views/containerinstances/containerinstances.html', + controller: 'AzureContainerInstancesController', + }, + }, + }; - var containerInstanceCreation = { - name: 'azure.containerinstances.new', - url: '/new/', - views: { - 'content@': { - templateUrl: './views/containerinstances/create/createcontainerinstance.html', - controller: 'AzureCreateContainerInstanceController' - } - } - }; + var containerInstanceCreation = { + name: 'azure.containerinstances.new', + url: '/new/', + views: { + 'content@': { + templateUrl: './views/containerinstances/create/createcontainerinstance.html', + controller: 'AzureCreateContainerInstanceController', + }, + }, + }; - var dashboard = { - name: 'azure.dashboard', - url: '/dashboard', - views: { - 'content@': { - templateUrl: './views/dashboard/dashboard.html', - controller: 'AzureDashboardController' - } - } - }; + var dashboard = { + name: 'azure.dashboard', + url: '/dashboard', + views: { + 'content@': { + templateUrl: './views/dashboard/dashboard.html', + controller: 'AzureDashboardController', + }, + }, + }; - $stateRegistryProvider.register(azure); - $stateRegistryProvider.register(containerInstances); - $stateRegistryProvider.register(containerInstanceCreation); - $stateRegistryProvider.register(dashboard); -}]); + $stateRegistryProvider.register(azure); + $stateRegistryProvider.register(containerInstances); + $stateRegistryProvider.register(containerInstanceCreation); + $stateRegistryProvider.register(dashboard); + }, +]); diff --git a/app/azure/components/azure-endpoint-config/azure-endpoint-config.js b/app/azure/components/azure-endpoint-config/azure-endpoint-config.js index 2909d3853..ff09f0908 100644 --- a/app/azure/components/azure-endpoint-config/azure-endpoint-config.js +++ b/app/azure/components/azure-endpoint-config/azure-endpoint-config.js @@ -2,7 +2,7 @@ angular.module('portainer.azure').component('azureEndpointConfig', { bindings: { applicationId: '=', tenantId: '=', - authenticationKey: '=' + authenticationKey: '=', }, - templateUrl: './azureEndpointConfig.html' + templateUrl: './azureEndpointConfig.html', }); diff --git a/app/azure/components/azure-endpoint-config/azureEndpointConfig.html b/app/azure/components/azure-endpoint-config/azureEndpointConfig.html index c0d839102..efc8bd79f 100644 --- a/app/azure/components/azure-endpoint-config/azureEndpointConfig.html +++ b/app/azure/components/azure-endpoint-config/azureEndpointConfig.html @@ -6,7 +6,7 @@
- +
@@ -14,7 +14,7 @@
- +
@@ -22,7 +22,14 @@
- +
diff --git a/app/azure/components/azure-sidebar-content/azure-sidebar-content.js b/app/azure/components/azure-sidebar-content/azure-sidebar-content.js index 68401cc1e..daec3ef12 100644 --- a/app/azure/components/azure-sidebar-content/azure-sidebar-content.js +++ b/app/azure/components/azure-sidebar-content/azure-sidebar-content.js @@ -1,3 +1,3 @@ angular.module('portainer.azure').component('azureSidebarContent', { - templateUrl: './azureSidebarContent.html' + templateUrl: './azureSidebarContent.html', }); diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html index 54361570c..f9936d78b 100644 --- a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html +++ b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html @@ -2,13 +2,10 @@
-
- {{ $ctrl.titleText }} -
+
{{ $ctrl.titleText }}
-
@@ -47,20 +52,23 @@ - + @@ -73,9 +81,7 @@
- + - {{ item.Name | truncate:50 }} + {{ item.Name | truncate: 50 }} {{ item.Location }} :{{ p.port }} - - + -
- -
-
+
+
- \ No newline at end of file + diff --git a/app/docker/components/container-quick-actions/containerQuickActions.html b/app/docker/components/container-quick-actions/containerQuickActions.html index 65ee13949..d4bbc55d1 100644 --- a/app/docker/components/container-quick-actions/containerQuickActions.html +++ b/app/docker/components/container-quick-actions/containerQuickActions.html @@ -1,10 +1,11 @@ - diff --git a/app/docker/components/container-quick-actions/containerQuickActions.js b/app/docker/components/container-quick-actions/containerQuickActions.js index 619cf7efa..6f8631df7 100644 --- a/app/docker/components/container-quick-actions/containerQuickActions.js +++ b/app/docker/components/container-quick-actions/containerQuickActions.js @@ -5,6 +5,6 @@ angular.module('portainer.docker').component('containerQuickActions', { nodeName: '<', status: '<', state: '<', - taskId: '<' - } + taskId: '<', + }, }); diff --git a/app/docker/components/container-restart-policy/container-restart-policy-controller.js b/app/docker/components/container-restart-policy/container-restart-policy-controller.js index 64a8604fc..159b8a2f5 100644 --- a/app/docker/components/container-restart-policy/container-restart-policy-controller.js +++ b/app/docker/components/container-restart-policy/container-restart-policy-controller.js @@ -1,26 +1,25 @@ -angular -.module('portainer.docker') -.controller('ContainerRestartPolicyController', [function ContainerRestartPolicyController() { - var ctrl = this; +angular.module('portainer.docker').controller('ContainerRestartPolicyController', [ + function ContainerRestartPolicyController() { + var ctrl = this; - this.state = { - editModel : {} - }; - - ctrl.save = save; - - function save() { - if (ctrl.state.editModel.name === ctrl.name && ctrl.state.editModel.maximumRetryCount === ctrl.maximumRetryCount) { - return; - } - ctrl.updateRestartPolicy(ctrl.state.editModel); - } - - this.$onInit = function() { - ctrl.state.editModel = { - name: ctrl.name ? ctrl.name : 'no', - maximumRetryCount: ctrl.maximumRetryCount + this.state = { + editModel: {}, }; - }; -} + + ctrl.save = save; + + function save() { + if (ctrl.state.editModel.name === ctrl.name && ctrl.state.editModel.maximumRetryCount === ctrl.maximumRetryCount) { + return; + } + ctrl.updateRestartPolicy(ctrl.state.editModel); + } + + this.$onInit = function () { + ctrl.state.editModel = { + name: ctrl.name ? ctrl.name : 'no', + maximumRetryCount: ctrl.maximumRetryCount, + }; + }; + }, ]); diff --git a/app/docker/components/container-restart-policy/container-restart-policy.js b/app/docker/components/container-restart-policy/container-restart-policy.js index 60e4f0c4b..bc4d02709 100644 --- a/app/docker/components/container-restart-policy/container-restart-policy.js +++ b/app/docker/components/container-restart-policy/container-restart-policy.js @@ -1,10 +1,9 @@ -angular.module('portainer.docker') -.component('containerRestartPolicy', { +angular.module('portainer.docker').component('containerRestartPolicy', { templateUrl: './container-restart-policy.html', controller: 'ContainerRestartPolicyController', bindings: { - 'name': '<', - 'maximumRetryCount': '<', - 'updateRestartPolicy': '&' - } + name: '<', + maximumRetryCount: '<', + updateRestartPolicy: '&', + }, }); diff --git a/app/docker/components/dashboard-cluster-agent-info/dashboard-cluster-agent-info.js b/app/docker/components/dashboard-cluster-agent-info/dashboard-cluster-agent-info.js index 88aae5cec..6498c2edc 100644 --- a/app/docker/components/dashboard-cluster-agent-info/dashboard-cluster-agent-info.js +++ b/app/docker/components/dashboard-cluster-agent-info/dashboard-cluster-agent-info.js @@ -1,4 +1,4 @@ angular.module('portainer.docker').component('dashboardClusterAgentInfo', { templateUrl: './dashboardClusterAgentInfo.html', - controller: 'DashboardClusterAgentInfoController' + controller: 'DashboardClusterAgentInfoController', }); diff --git a/app/docker/components/dashboard-cluster-agent-info/dashboardClusterAgentInfoController.js b/app/docker/components/dashboard-cluster-agent-info/dashboardClusterAgentInfoController.js index 7ba6f7f08..2b421729b 100644 --- a/app/docker/components/dashboard-cluster-agent-info/dashboardClusterAgentInfoController.js +++ b/app/docker/components/dashboard-cluster-agent-info/dashboardClusterAgentInfoController.js @@ -1,16 +1,17 @@ -angular.module('portainer.docker') -.controller('DashboardClusterAgentInfoController', ['AgentService', 'Notifications', -function (AgentService, Notifications) { - var ctrl = this; +angular.module('portainer.docker').controller('DashboardClusterAgentInfoController', [ + 'AgentService', + 'Notifications', + function (AgentService, Notifications) { + var ctrl = this; - this.$onInit = function() { - AgentService.agents() - .then(function success(data) { - ctrl.agentCount = data.length; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve agent information'); - }); - }; - -}]); + this.$onInit = function () { + AgentService.agents() + .then(function success(data) { + ctrl.agentCount = data.length; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve agent information'); + }); + }; + }, +]); diff --git a/app/docker/components/datatables/configs-datatable/configsDatatable.html b/app/docker/components/datatables/configs-datatable/configsDatatable.html index d6a1e386d..a6b8aa6a7 100644 --- a/app/docker/components/datatables/configs-datatable/configsDatatable.html +++ b/app/docker/components/datatables/configs-datatable/configsDatatable.html @@ -2,9 +2,7 @@
-
- {{ $ctrl.titleText }} -
+
{{ $ctrl.titleText }}
Settings @@ -16,7 +14,7 @@
{{ key }}
{{ key }} {{ value.IPAddress || '-' }} {{ value.Gateway || '-' }} {{ value.MacAddress || '-' }} - diff --git a/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.js b/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.js index 7ac1b5580..501f86c46 100644 --- a/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.js +++ b/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.js @@ -12,6 +12,6 @@ angular.module('portainer.docker').component('containerNetworksDatatable', { joinNetworkActionInProgress: '<', leaveNetworkActionInProgress: '<', leaveNetworkAction: '<', - nodeName: '<' - } + nodeName: '<', + }, }); diff --git a/app/docker/components/datatables/container-processes-datatable/containerProcessesDatatable.html b/app/docker/components/datatables/container-processes-datatable/containerProcessesDatatable.html index 37f3152b5..326deef73 100644 --- a/app/docker/components/datatables/container-processes-datatable/containerProcessesDatatable.html +++ b/app/docker/components/datatables/container-processes-datatable/containerProcessesDatatable.html @@ -2,13 +2,11 @@
-
- {{ $ctrl.titleText }} -
+
{{ $ctrl.titleText }}
diff --git a/app/docker/components/datatables/container-processes-datatable/containerProcessesDatatable.js b/app/docker/components/datatables/container-processes-datatable/containerProcessesDatatable.js index 1a0abdc3a..b89a4d63b 100644 --- a/app/docker/components/datatables/container-processes-datatable/containerProcessesDatatable.js +++ b/app/docker/components/datatables/container-processes-datatable/containerProcessesDatatable.js @@ -8,6 +8,6 @@ angular.module('portainer.docker').component('containerProcessesDatatable', { headerset: '<', tableKey: '@', orderBy: '@', - reverseOrder: '<' - } + reverseOrder: '<', + }, }); diff --git a/app/docker/components/datatables/containers-datatable/actions/containersDatatableActions.html b/app/docker/components/datatables/containers-datatable/actions/containersDatatableActions.html index 873657caf..9d9f8d9f8 100644 --- a/app/docker/components/datatables/containers-datatable/actions/containersDatatableActions.html +++ b/app/docker/components/datatables/containers-datatable/actions/containersDatatableActions.html @@ -1,31 +1,69 @@ -
+
- - - - - - -
diff --git a/app/docker/components/datatables/containers-datatable/actions/containersDatatableActions.js b/app/docker/components/datatables/containers-datatable/actions/containersDatatableActions.js index 2f82916a1..b6f83f273 100644 --- a/app/docker/components/datatables/containers-datatable/actions/containersDatatableActions.js +++ b/app/docker/components/datatables/containers-datatable/actions/containersDatatableActions.js @@ -7,6 +7,6 @@ angular.module('portainer.docker').component('containersDatatableActions', { noStoppedItemsSelected: '=', noRunningItemsSelected: '=', noPausedItemsSelected: '=', - showAddAction: '<' - } + showAddAction: '<', + }, }); diff --git a/app/docker/components/datatables/containers-datatable/actions/containersDatatableActionsController.js b/app/docker/components/datatables/containers-datatable/actions/containersDatatableActionsController.js index efacb89e7..432df50f8 100644 --- a/app/docker/components/datatables/containers-datatable/actions/containersDatatableActionsController.js +++ b/app/docker/components/datatables/containers-datatable/actions/containersDatatableActionsController.js @@ -1,106 +1,112 @@ -angular.module('portainer.docker') -.controller('ContainersDatatableActionsController', ['$state', 'ContainerService', 'ModalService', 'Notifications', 'HttpRequestHelper', -function ($state, ContainerService, ModalService, Notifications, HttpRequestHelper) { - this.startAction = function(selectedItems) { - var successMessage = 'Container successfully started'; - var errorMessage = 'Unable to start container'; - executeActionOnContainerList(selectedItems, ContainerService.startContainer, successMessage, errorMessage); - }; +angular.module('portainer.docker').controller('ContainersDatatableActionsController', [ + '$state', + 'ContainerService', + 'ModalService', + 'Notifications', + 'HttpRequestHelper', + function ($state, ContainerService, ModalService, Notifications, HttpRequestHelper) { + this.startAction = function (selectedItems) { + var successMessage = 'Container successfully started'; + var errorMessage = 'Unable to start container'; + executeActionOnContainerList(selectedItems, ContainerService.startContainer, successMessage, errorMessage); + }; - this.stopAction = function(selectedItems) { - var successMessage = 'Container successfully stopped'; - var errorMessage = 'Unable to stop container'; - executeActionOnContainerList(selectedItems, ContainerService.stopContainer, successMessage, errorMessage); - }; + this.stopAction = function (selectedItems) { + var successMessage = 'Container successfully stopped'; + var errorMessage = 'Unable to stop container'; + executeActionOnContainerList(selectedItems, ContainerService.stopContainer, successMessage, errorMessage); + }; - this.restartAction = function(selectedItems) { - var successMessage = 'Container successfully restarted'; - var errorMessage = 'Unable to restart container'; - executeActionOnContainerList(selectedItems, ContainerService.restartContainer, successMessage, errorMessage); - }; + this.restartAction = function (selectedItems) { + var successMessage = 'Container successfully restarted'; + var errorMessage = 'Unable to restart container'; + executeActionOnContainerList(selectedItems, ContainerService.restartContainer, successMessage, errorMessage); + }; - this.killAction = function(selectedItems) { - var successMessage = 'Container successfully killed'; - var errorMessage = 'Unable to kill container'; - executeActionOnContainerList(selectedItems, ContainerService.killContainer, successMessage, errorMessage); - }; + this.killAction = function (selectedItems) { + var successMessage = 'Container successfully killed'; + var errorMessage = 'Unable to kill container'; + executeActionOnContainerList(selectedItems, ContainerService.killContainer, successMessage, errorMessage); + }; - this.pauseAction = function(selectedItems) { - var successMessage = 'Container successfully paused'; - var errorMessage = 'Unable to pause container'; - executeActionOnContainerList(selectedItems, ContainerService.pauseContainer, successMessage, errorMessage); - }; + this.pauseAction = function (selectedItems) { + var successMessage = 'Container successfully paused'; + var errorMessage = 'Unable to pause container'; + executeActionOnContainerList(selectedItems, ContainerService.pauseContainer, successMessage, errorMessage); + }; - this.resumeAction = function(selectedItems) { - var successMessage = 'Container successfully resumed'; - var errorMessage = 'Unable to resume container'; - executeActionOnContainerList(selectedItems, ContainerService.resumeContainer, successMessage, errorMessage); - }; + this.resumeAction = function (selectedItems) { + var successMessage = 'Container successfully resumed'; + var errorMessage = 'Unable to resume container'; + executeActionOnContainerList(selectedItems, ContainerService.resumeContainer, successMessage, errorMessage); + }; - this.removeAction = function(selectedItems) { - var isOneContainerRunning = false; - for (var i = 0; i < selectedItems.length; i++) { - var container = selectedItems[i]; - if (container.State === 'running') { - isOneContainerRunning = true; - break; + this.removeAction = function (selectedItems) { + var isOneContainerRunning = false; + for (var i = 0; i < selectedItems.length; i++) { + var container = selectedItems[i]; + if (container.State === 'running') { + isOneContainerRunning = true; + break; + } } - } - var title = 'You are about to remove one or more container.'; - if (isOneContainerRunning) { - title = 'You are about to remove one or more running container.'; - } + var title = 'You are about to remove one or more container.'; + if (isOneContainerRunning) { + title = 'You are about to remove one or more running container.'; + } - ModalService.confirmContainerDeletion(title, function (result) { - if(!result) { return; } + ModalService.confirmContainerDeletion(title, function (result) { + if (!result) { + return; + } var cleanVolumes = false; if (result[0]) { cleanVolumes = true; } removeSelectedContainers(selectedItems, cleanVolumes); - } - ); - }; - - function executeActionOnContainerList(containers, action, successMessage, errorMessage) { - var actionCount = containers.length; - angular.forEach(containers, function (container) { - HttpRequestHelper.setPortainerAgentTargetHeader(container.NodeName); - action(container.Id) - .then(function success() { - Notifications.success(successMessage, container.Names[0]); - }) - .catch(function error(err) { - errorMessage = errorMessage + ':' + container.Names[0]; - Notifications.error('Failure', err, errorMessage); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } }); - }); - } + }; - function removeSelectedContainers(containers, cleanVolumes) { - var actionCount = containers.length; - angular.forEach(containers, function (container) { - HttpRequestHelper.setPortainerAgentTargetHeader(container.NodeName); - ContainerService.remove(container, cleanVolumes) - .then(function success() { - Notifications.success('Container successfully removed', container.Names[0]); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove container'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } + function executeActionOnContainerList(containers, action, successMessage, errorMessage) { + var actionCount = containers.length; + angular.forEach(containers, function (container) { + HttpRequestHelper.setPortainerAgentTargetHeader(container.NodeName); + action(container.Id) + .then(function success() { + Notifications.success(successMessage, container.Names[0]); + }) + .catch(function error(err) { + errorMessage = errorMessage + ':' + container.Names[0]; + Notifications.error('Failure', err, errorMessage); + }) + .finally(function final() { + --actionCount; + if (actionCount === 0) { + $state.reload(); + } + }); }); - }); - } -}]); + } + + function removeSelectedContainers(containers, cleanVolumes) { + var actionCount = containers.length; + angular.forEach(containers, function (container) { + HttpRequestHelper.setPortainerAgentTargetHeader(container.NodeName); + ContainerService.remove(container, cleanVolumes) + .then(function success() { + Notifications.success('Container successfully removed', container.Names[0]); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove container'); + }) + .finally(function final() { + --actionCount; + if (actionCount === 0) { + $state.reload(); + } + }); + }); + } + }, +]); diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index b8bdc3a4b..638b45e72 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -2,60 +2,65 @@
-
- {{ $ctrl.titleText }} -
+
{{ $ctrl.titleText }}
- - Columns -
- @@ -235,39 +263,69 @@ - + - - +
- + @@ -170,7 +188,7 @@ @@ -180,7 +198,17 @@ + Quick actions @@ -199,8 +227,8 @@ - - + + Created
- - + + - {{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }} + {{ + item | containername | truncate: $ctrl.settings.containerNameTruncateSize + }} {{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }} - {{ item.Status }} - {{ item.Status }} + {{ item.Status }} + {{ item.Status }} + - {{ item.StackName ? item.StackName : '-' }} {{ item.Image | trimshasum }} {{ item.Image | trimshasum }} - {{item.Created | getisodatefromtimestamp}} + {{ item.Created | getisodatefromtimestamp }} {{ item.IP ? item.IP : '-' }} {{ item.NodeName ? item.NodeName : '-' }} - + {{ p.public }}:{{ p.private }} - - + - @@ -286,9 +344,7 @@
{{device.Name}}{{device.Vendor}}{{ device.Name }}{{ device.Vendor }}
Loading...
{{disk.Vendor}}{{disk.Size | humansize}}{{ disk.Vendor }}{{ disk.Size | humansize }}
Loading...
Version{{ $ctrl.engine.releaseVersion }} (API: {{ $ctrl.engine.apiVersion }}){{ $ctrl.engine.releaseVersion }} (API: {{ $ctrl.engine.apiVersion }})
Root directory
Engine Labels{{ $ctrl.engine.engineLabels | labelsToStr:', ' }}{{ $ctrl.engine.engineLabels | labelsToStr: ', ' }}
-
\ No newline at end of file +
diff --git a/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.js b/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.js index 12bb2fb1e..543a5909e 100644 --- a/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.js +++ b/app/docker/components/host-view-panels/engine-details-panel/engine-details-panel.js @@ -1,6 +1,6 @@ angular.module('portainer.docker').component('engineDetailsPanel', { templateUrl: './engine-details-panel.html', bindings: { - engine: '<' - } + engine: '<', + }, }); diff --git a/app/docker/components/host-view-panels/host-details-panel/host-details-panel.html b/app/docker/components/host-view-panels/host-details-panel/host-details-panel.html index 9b5b006d7..24ba17e70 100644 --- a/app/docker/components/host-view-panels/host-details-panel/host-details-panel.html +++ b/app/docker/components/host-view-panels/host-details-panel/host-details-panel.html @@ -11,8 +11,7 @@ OS Information - {{ $ctrl.host.os.type }} {{$ctrl.host.os.arch}} - {{$ctrl.host.os.name}} + {{ $ctrl.host.os.type }} {{ $ctrl.host.os.arch }} {{ $ctrl.host.os.name }} Kernel Version @@ -28,10 +27,10 @@ - - diff --git a/app/docker/components/host-view-panels/host-details-panel/host-details-panel.js b/app/docker/components/host-view-panels/host-details-panel/host-details-panel.js index 5d25a626e..693a022b7 100644 --- a/app/docker/components/host-view-panels/host-details-panel/host-details-panel.js +++ b/app/docker/components/host-view-panels/host-details-panel/host-details-panel.js @@ -5,6 +5,6 @@ angular.module('portainer.docker').component('hostDetailsPanel', { isJobEnabled: '<', isBrowseEnabled: '<', browseUrl: '@', - jobUrl: '@' - } + jobUrl: '@', + }, }); diff --git a/app/docker/components/host-view-panels/node-availability-select/node-availability-select-controller.js b/app/docker/components/host-view-panels/node-availability-select/node-availability-select-controller.js index 52df40cd6..2ebd29ee6 100644 --- a/app/docker/components/host-view-panels/node-availability-select/node-availability-select-controller.js +++ b/app/docker/components/host-view-panels/node-availability-select/node-availability-select-controller.js @@ -1,11 +1,9 @@ -angular - .module('portainer.docker') - .controller('NodeAvailabilitySelectController', [ - function NodeAvailabilitySelectController() { - this.onChange = onChange; +angular.module('portainer.docker').controller('NodeAvailabilitySelectController', [ + function NodeAvailabilitySelectController() { + this.onChange = onChange; - function onChange() { - this.onSave({ availability: this.availability }); - } + function onChange() { + this.onSave({ availability: this.availability }); } - ]); + }, +]); diff --git a/app/docker/components/host-view-panels/node-availability-select/node-availability-select.html b/app/docker/components/host-view-panels/node-availability-select/node-availability-select.html index 94e086127..b37f7d9d6 100644 --- a/app/docker/components/host-view-panels/node-availability-select/node-availability-select.html +++ b/app/docker/components/host-view-panels/node-availability-select/node-availability-select.html @@ -1,8 +1,7 @@
- -
\ No newline at end of file +
diff --git a/app/docker/components/host-view-panels/node-availability-select/node-availability-select.js b/app/docker/components/host-view-panels/node-availability-select/node-availability-select.js index b396d65f7..b9af163be 100644 --- a/app/docker/components/host-view-panels/node-availability-select/node-availability-select.js +++ b/app/docker/components/host-view-panels/node-availability-select/node-availability-select.js @@ -4,6 +4,6 @@ angular.module('portainer.docker').component('nodeAvailabilitySelect', { bindings: { availability: '<', originalValue: '<', - onSave: '&' - } + onSave: '&', + }, }); diff --git a/app/docker/components/host-view-panels/node-labels-table/node-labels-table-controller.js b/app/docker/components/host-view-panels/node-labels-table/node-labels-table-controller.js index a9ad8ab58..4296d4bc0 100644 --- a/app/docker/components/host-view-panels/node-labels-table/node-labels-table-controller.js +++ b/app/docker/components/host-view-panels/node-labels-table/node-labels-table-controller.js @@ -12,12 +12,9 @@ angular.module('portainer.docker').controller('NodeLabelsTableController', [ } function updateLabel(label) { - if ( - label.value !== label.originalValue || - label.key !== label.originalKey - ) { + if (label.value !== label.originalValue || label.key !== label.originalKey) { ctrl.onChangedLabels({ labels: ctrl.labels }); } } - } + }, ]); diff --git a/app/docker/components/host-view-panels/node-labels-table/node-labels-table.html b/app/docker/components/host-view-panels/node-labels-table/node-labels-table.html index 86eee9356..e26ec168c 100644 --- a/app/docker/components/host-view-panels/node-labels-table/node-labels-table.html +++ b/app/docker/components/host-view-panels/node-labels-table/node-labels-table.html @@ -14,15 +14,13 @@
Name - +
Value - + -
-
\ No newline at end of file +
diff --git a/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.js b/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.js index ad5bf7a7a..751e4aea4 100644 --- a/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.js +++ b/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.js @@ -3,6 +3,6 @@ angular.module('portainer.docker').component('swarmNodeDetailsPanel', { controller: 'SwarmNodeDetailsPanelController', bindings: { details: '<', - originalNode: '<' - } + originalNode: '<', + }, }); diff --git a/app/docker/components/imageRegistry/por-image-registry.js b/app/docker/components/imageRegistry/por-image-registry.js index 39d6a5919..4c2f7b4bd 100644 --- a/app/docker/components/imageRegistry/por-image-registry.js +++ b/app/docker/components/imageRegistry/por-image-registry.js @@ -2,13 +2,13 @@ angular.module('portainer.docker').component('porImageRegistry', { templateUrl: './porImageRegistry.html', controller: 'porImageRegistryController', bindings: { - 'model': '=', // must be of type PorImageRegistryModel - 'pullWarning': '<', - 'autoComplete': '<', - 'labelClass': '@', - 'inputClass': '@' + model: '=', // must be of type PorImageRegistryModel + pullWarning: '<', + autoComplete: '<', + labelClass: '@', + inputClass: '@', }, require: { - form: '^form' - } + form: '^form', + }, }); diff --git a/app/docker/components/imageRegistry/porImageRegistry.html b/app/docker/components/imageRegistry/porImageRegistry.html index 05bc7ea07..feee8ba1b 100644 --- a/app/docker/components/imageRegistry/porImageRegistry.html +++ b/app/docker/components/imageRegistry/porImageRegistry.html @@ -5,15 +5,28 @@ Registry
- +
- {{$ctrl.displayedRegistryURL()}} - + {{ $ctrl.displayedRegistryURL() }} +
@@ -22,11 +35,12 @@
-
@@ -35,7 +49,10 @@
-

Image name is required. Tag must be specified otherwise Portainer will pull all tags associated to the image.

+

Image name is required. + Tag must be specified otherwise Portainer will pull all tags associated to the image.

diff --git a/app/docker/components/imageRegistry/porImageRegistryController.js b/app/docker/components/imageRegistry/porImageRegistryController.js index 7f4e9cfbc..f16902c9d 100644 --- a/app/docker/components/imageRegistry/porImageRegistryController.js +++ b/app/docker/components/imageRegistry/porImageRegistryController.js @@ -38,7 +38,7 @@ class porImageRegistryController { if (this.isKnownRegistry(registry)) { const url = this.getRegistryURL(registry); const registryImages = _.filter(this.images, (image) => _.includes(image, url)); - images = _.map(registryImages, (image) => _.replace(image, new RegExp(url + '\/?'), '')); + images = _.map(registryImages, (image) => _.replace(image, new RegExp(url + '/?'), '')); } else { const registries = _.filter(this.availableRegistries, (reg) => this.isKnownRegistry(reg)); const registryImages = _.flatMap(registries, (registry) => _.filter(this.images, (image) => _.includes(image, registry.URL))); @@ -64,7 +64,7 @@ class porImageRegistryController { const [registries, dockerhub, images] = await Promise.all([ this.RegistryService.registries(), this.DockerHubService.dockerhub(), - this.autoComplete ? this.ImageService.images() : [] + this.autoComplete ? this.ImageService.images() : [], ]); this.images = this.ImageService.getUniqueTagListFromImages(images); this.availableRegistries = _.concat(dockerhub, registries); @@ -73,7 +73,7 @@ class porImageRegistryController { if (!id) { this.model.Registry = dockerhub; } else { - this.model.Registry = _.find(this.availableRegistries, { 'Id': id }); + this.model.Registry = _.find(this.availableRegistries, { Id: id }); } } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve registries'); diff --git a/app/docker/components/log-viewer/log-viewer.js b/app/docker/components/log-viewer/log-viewer.js index 0be45bc3c..7521916ea 100644 --- a/app/docker/components/log-viewer/log-viewer.js +++ b/app/docker/components/log-viewer/log-viewer.js @@ -6,6 +6,6 @@ angular.module('portainer.docker').component('logViewer', { displayTimestamps: '=', logCollectionChange: '<', sinceTimestamp: '=', - lineCount: '=' - } + lineCount: '=', + }, }); diff --git a/app/docker/components/log-viewer/logViewer.html b/app/docker/components/log-viewer/logViewer.html index 44c1c5d7e..ac9095ebc 100644 --- a/app/docker/components/log-viewer/logViewer.html +++ b/app/docker/components/log-viewer/logViewer.html @@ -11,7 +11,11 @@ @@ -20,9 +24,7 @@ - +
@@ -30,9 +32,7 @@ - +
@@ -51,7 +51,7 @@ Search
- +
@@ -59,7 +59,7 @@ Lines
- +
@@ -67,9 +67,21 @@ Actions
- - - + + + @@ -81,9 +93,9 @@
-
-
-
+
+
+
       

{{ line }}

No log line matching the '{{ $ctrl.state.search }}' filter

No logs available

diff --git a/app/docker/components/log-viewer/logViewerController.js b/app/docker/components/log-viewer/logViewerController.js index 9fdc62663..d55fe555e 100644 --- a/app/docker/components/log-viewer/logViewerController.js +++ b/app/docker/components/log-viewer/logViewerController.js @@ -1,47 +1,47 @@ import moment from 'moment'; -angular.module('portainer.docker') -.controller('LogViewerController', ['clipboard', -function (clipboard) { +angular.module('portainer.docker').controller('LogViewerController', [ + 'clipboard', + function (clipboard) { + this.state = { + availableSinceDatetime: [ + { desc: 'Last day', value: moment().subtract(1, 'days').format() }, + { desc: 'Last 4 hours', value: moment().subtract(4, 'hours').format() }, + { desc: 'Last hour', value: moment().subtract(1, 'hours').format() }, + { desc: 'Last 10 minutes', value: moment().subtract(10, 'minutes').format() }, + ], + copySupported: clipboard.supported, + logCollection: true, + autoScroll: true, + wrapLines: true, + search: '', + filteredLogs: [], + selectedLines: [], + }; - this.state = { - availableSinceDatetime: [ - { desc: 'Last day', value: moment().subtract(1, 'days').format() }, - { desc: 'Last 4 hours', value: moment().subtract(4, 'hours').format() }, - { desc: 'Last hour', value: moment().subtract(1, 'hours').format() }, - { desc: 'Last 10 minutes', value: moment().subtract(10, 'minutes').format() } - ], - copySupported: clipboard.supported, - logCollection: true, - autoScroll: true, - wrapLines: true, - search: '', - filteredLogs: [], - selectedLines: [] - }; + this.copy = function () { + clipboard.copyText(this.state.filteredLogs); + $('#refreshRateChange').show(); + $('#refreshRateChange').fadeOut(2000); + }; - this.copy = function() { - clipboard.copyText(this.state.filteredLogs); - $('#refreshRateChange').show(); - $('#refreshRateChange').fadeOut(2000); - }; + this.copySelection = function () { + clipboard.copyText(this.state.selectedLines); + $('#refreshRateChange').show(); + $('#refreshRateChange').fadeOut(2000); + }; - this.copySelection = function() { - clipboard.copyText(this.state.selectedLines); - $('#refreshRateChange').show(); - $('#refreshRateChange').fadeOut(2000); - }; + this.clearSelection = function () { + this.state.selectedLines = []; + }; - this.clearSelection = function() { - this.state.selectedLines = []; - }; - - this.selectLine = function(line) { - var idx = this.state.selectedLines.indexOf(line); - if (idx === -1) { - this.state.selectedLines.push(line); - } else { - this.state.selectedLines.splice(idx, 1); - } - }; -}]); + this.selectLine = function (line) { + var idx = this.state.selectedLines.indexOf(line); + if (idx === -1) { + this.state.selectedLines.push(line); + } else { + this.state.selectedLines.splice(idx, 1); + } + }; + }, +]); diff --git a/app/docker/components/network-macvlan-form/network-macvlan-form.js b/app/docker/components/network-macvlan-form/network-macvlan-form.js index d9f07d4c8..3ae345cfb 100644 --- a/app/docker/components/network-macvlan-form/network-macvlan-form.js +++ b/app/docker/components/network-macvlan-form/network-macvlan-form.js @@ -3,6 +3,6 @@ angular.module('portainer.docker').component('networkMacvlanForm', { controller: 'NetworkMacvlanFormController', bindings: { data: '=', - applicationState: '<' - } -}); \ No newline at end of file + applicationState: '<', + }, +}); diff --git a/app/docker/components/network-macvlan-form/networkMacvlanForm.html b/app/docker/components/network-macvlan-form/networkMacvlanForm.html index b104bac9d..fca929aa5 100644 --- a/app/docker/components/network-macvlan-form/networkMacvlanForm.html +++ b/app/docker/components/network-macvlan-form/networkMacvlanForm.html @@ -10,10 +10,10 @@
-
+
- +
-
\ No newline at end of file +
diff --git a/app/docker/filters/filters.js b/app/docker/filters/filters.js index 46bdd65f0..a7ad2310c 100644 --- a/app/docker/filters/filters.js +++ b/app/docker/filters/filters.js @@ -1,7 +1,7 @@ import _ from 'lodash-es'; function includeString(text, values) { - return values.some(function(val){ + return values.some(function (val) { return text.indexOf(val) !== -1; }); } @@ -16,290 +16,293 @@ function strToHash(str) { function hashToHexColor(hash) { var color = '#'; - for (var i = 0; i < 3;) { - color += ('00' + ((hash >> i++ * 8) & 0xFF).toString(16)).slice(-2); + for (var i = 0; i < 3; ) { + color += ('00' + ((hash >> (i++ * 8)) & 0xff).toString(16)).slice(-2); } return color; } -angular.module('portainer.docker') -.filter('visualizerTask', function () { - 'use strict'; - return function (text) { - var status = _.toLower(text); - if (includeString(status, ['new', 'allocated', 'assigned', 'accepted', 'complete', 'preparing'])) { - return 'info'; - } else if (includeString(status, ['pending'])) { - return 'warning'; - } else if (includeString(status, ['shutdown', 'failed', 'rejected'])) { - return 'stopped'; - } - return 'running'; - }; -}) -.filter('visualizerTaskBorderColor', function () { - 'use strict'; - return function (str) { - var hash = strToHash(str); - var color = hashToHexColor(hash); - return color; - }; -}) -.filter('taskstatusbadge', function () { - 'use strict'; - return function (text) { - var status = _.toLower(text); - var labelStyle = 'default'; - if (includeString(status, ['new', 'allocated', 'assigned', 'accepted', 'preparing', 'ready', 'starting', 'remove'])) { - labelStyle = 'info'; - } else if (includeString(status, ['pending'])) { - labelStyle = 'warning'; - } else if (includeString(status, ['shutdown', 'failed', 'rejected', 'orphaned'])) { - labelStyle = 'danger'; - } else if (includeString(status, ['complete'])) { - labelStyle = 'primary'; - } else if (includeString(status, ['running'])) { - labelStyle = 'success'; - } - return labelStyle; - }; -}) -.filter('taskhaslogs', function () { - 'use strict'; - return function (state) { - var validState = ['running', 'complete', 'failed', 'shutdown']; - if (validState.indexOf(state) > -1) { - return true; - } - return false; - }; -}) -.filter('containerstatusbadge', function () { - 'use strict'; - return function (text) { - var status = _.toLower(text); - if (includeString(status, ['paused', 'starting', 'unhealthy'])) { - return 'warning'; - } else if (includeString(status, ['created'])) { - return 'info'; - } else if (includeString(status, ['stopped', 'dead', 'exited'])) { - return 'danger'; - } - return 'success'; - }; -}) -.filter('nodestatusbadge', function () { - 'use strict'; - return function (text) { - if (text === 'down' || text === 'Unhealthy') { - return 'danger'; - } - return 'success'; - }; -}) -.filter('dockerNodeAvailabilityBadge', function () { - 'use strict'; - return function (text) { - if (text === 'pause') { - return 'warning'; - } - else if (text === 'drain') { - return 'danger'; - } - return 'success'; - }; -}) -.filter('trimcontainername', function () { - 'use strict'; - return function (name) { - if (name) { - return (name.indexOf('/') === 0 ? name.replace('/','') : name); - } - return ''; - }; -}) -.filter('getstatetext', function () { - 'use strict'; - return function (state) { - if (state === undefined) { +angular + .module('portainer.docker') + .filter('visualizerTask', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + if (includeString(status, ['new', 'allocated', 'assigned', 'accepted', 'complete', 'preparing'])) { + return 'info'; + } else if (includeString(status, ['pending'])) { + return 'warning'; + } else if (includeString(status, ['shutdown', 'failed', 'rejected'])) { + return 'stopped'; + } + return 'running'; + }; + }) + .filter('visualizerTaskBorderColor', function () { + 'use strict'; + return function (str) { + var hash = strToHash(str); + var color = hashToHexColor(hash); + return color; + }; + }) + .filter('taskstatusbadge', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + var labelStyle = 'default'; + if (includeString(status, ['new', 'allocated', 'assigned', 'accepted', 'preparing', 'ready', 'starting', 'remove'])) { + labelStyle = 'info'; + } else if (includeString(status, ['pending'])) { + labelStyle = 'warning'; + } else if (includeString(status, ['shutdown', 'failed', 'rejected', 'orphaned'])) { + labelStyle = 'danger'; + } else if (includeString(status, ['complete'])) { + labelStyle = 'primary'; + } else if (includeString(status, ['running'])) { + labelStyle = 'success'; + } + return labelStyle; + }; + }) + .filter('taskhaslogs', function () { + 'use strict'; + return function (state) { + var validState = ['running', 'complete', 'failed', 'shutdown']; + if (validState.indexOf(state) > -1) { + return true; + } + return false; + }; + }) + .filter('containerstatusbadge', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + if (includeString(status, ['paused', 'starting', 'unhealthy'])) { + return 'warning'; + } else if (includeString(status, ['created'])) { + return 'info'; + } else if (includeString(status, ['stopped', 'dead', 'exited'])) { + return 'danger'; + } + return 'success'; + }; + }) + .filter('nodestatusbadge', function () { + 'use strict'; + return function (text) { + if (text === 'down' || text === 'Unhealthy') { + return 'danger'; + } + return 'success'; + }; + }) + .filter('dockerNodeAvailabilityBadge', function () { + 'use strict'; + return function (text) { + if (text === 'pause') { + return 'warning'; + } else if (text === 'drain') { + return 'danger'; + } + return 'success'; + }; + }) + .filter('trimcontainername', function () { + 'use strict'; + return function (name) { + if (name) { + return name.indexOf('/') === 0 ? name.replace('/', '') : name; + } return ''; - } - if (state.Dead) { - return 'Dead'; - } - if (state.Ghost && state.Running) { - return 'Ghost'; - } - if (state.Running && state.Paused) { - return 'Running (Paused)'; - } - if (state.Running) { - return 'Running'; - } - if (state.Status === 'created') { - return 'Created'; - } - return 'Stopped'; - }; -}) -.filter('getstatelabel', function () { - 'use strict'; - return function (state) { - if (state === undefined) { + }; + }) + .filter('getstatetext', function () { + 'use strict'; + return function (state) { + if (state === undefined) { + return ''; + } + if (state.Dead) { + return 'Dead'; + } + if (state.Ghost && state.Running) { + return 'Ghost'; + } + if (state.Running && state.Paused) { + return 'Running (Paused)'; + } + if (state.Running) { + return 'Running'; + } + if (state.Status === 'created') { + return 'Created'; + } + return 'Stopped'; + }; + }) + .filter('getstatelabel', function () { + 'use strict'; + return function (state) { + if (state === undefined) { + return 'label-default'; + } + if (state.Ghost && state.Running) { + return 'label-important'; + } + if (state.Running) { + return 'label-success'; + } return 'label-default'; - } - if (state.Ghost && state.Running) { - return 'label-important'; - } - if (state.Running) { - return 'label-success'; - } - return 'label-default'; - }; -}) -.filter('containername', function () { - 'use strict'; - return function (container) { - var name = container.Names[0]; - return name.substring(1, name.length); - }; -}) -.filter('swarmversion', function () { - 'use strict'; - return function (text) { - return _.split(text, '/')[1]; - }; -}) -.filter('swarmhostname', function () { - 'use strict'; - return function (container) { - return _.split(container.Names[0], '/')[1]; - }; -}) -.filter('repotags', function () { - 'use strict'; - return function (image) { - if (image.RepoTags && image.RepoTags.length > 0) { - var tag = image.RepoTags[0]; - if (tag === ':') { - return []; + }; + }) + .filter('containername', function () { + 'use strict'; + return function (container) { + var name = container.Names[0]; + return name.substring(1, name.length); + }; + }) + .filter('swarmversion', function () { + 'use strict'; + return function (text) { + return _.split(text, '/')[1]; + }; + }) + .filter('swarmhostname', function () { + 'use strict'; + return function (container) { + return _.split(container.Names[0], '/')[1]; + }; + }) + .filter('repotags', function () { + 'use strict'; + return function (image) { + if (image.RepoTags && image.RepoTags.length > 0) { + var tag = image.RepoTags[0]; + if (tag === ':') { + return []; + } + return image.RepoTags; } - return image.RepoTags; - } - return []; - }; -}) -.filter('command', function () { - 'use strict'; - return function (command) { - if (command) { - return command.join(' '); - } - }; -}) -.filter('hideshasum', function () { - 'use strict'; - return function (imageName) { - if (imageName) { - return imageName.split('@sha')[0]; - } - return ''; - }; -}) -.filter('availablenodecount', ['ConstraintsHelper', function (ConstraintsHelper) { - 'use strict'; - return function (nodes, service) { - var availableNodes = 0; - for (var i = 0; i < nodes.length; i++) { - var node = nodes[i]; - if (node.Availability === 'active' && node.Status === 'ready' && ConstraintsHelper.matchesServiceConstraints(service, node)) { - availableNodes++; + return []; + }; + }) + .filter('command', function () { + 'use strict'; + return function (command) { + if (command) { + return command.join(' '); } - } - return availableNodes; - }; -}]) -.filter('runningtaskscount', function () { - 'use strict'; - return function (tasks) { - var runningTasks = 0; - for (var i = 0; i < tasks.length; i++) { - var task = tasks[i]; - if (task.Status.State === 'running' && task.DesiredState === 'running') { - runningTasks++; + }; + }) + .filter('hideshasum', function () { + 'use strict'; + return function (imageName) { + if (imageName) { + return imageName.split('@sha')[0]; } - } - return runningTasks; - }; -}) -.filter('runningcontainers', function () { - 'use strict'; - return function runningContainersFilter(containers) { - return containers.filter(function (container) { - return container.State === 'running'; - }).length; - }; -}) -.filter('stoppedcontainers', function () { - 'use strict'; - return function stoppedContainersFilter(containers) { - return containers.filter(function (container) { - return container.State === 'exited'; - }).length; - }; -}) -.filter('healthycontainers', function () { - 'use strict'; - return function healthyContainersFilter(containers) { - return containers.filter(function (container) { - return container.Status === 'healthy'; - }).length; - } -}) -.filter('unhealthycontainers', function () { - 'use strict'; - return function unhealthyContainersFilter(containers) { - return containers.filter(function (container) { - return container.Status === 'unhealthy'; - }).length; - } -}) -.filter('imagestotalsize', function () { - 'use strict'; - return function (images) { - var totalImageSize = 0; - for (var i = 0; i < images.length; i++) { - var item = images[i]; - totalImageSize += item.VirtualSize; - } - return totalImageSize; - }; -}) -.filter('tasknodename', function () { - 'use strict'; - return function (nodeId, nodes) { - var node = _.find(nodes, { Id: nodeId }); - if (node) { - return node.Hostname; - } - return ''; - }; -}) -.filter('imagelayercommand', function () { - 'use strict'; - return function (createdBy) { - return createdBy.replace('/bin/sh -c #(nop) ', '').replace('/bin/sh -c ', 'RUN '); - }; -}) -.filter('trimshasum', function () { - 'use strict'; - return function (imageName) { - if (!imageName) { - return; - } - if (imageName.indexOf('sha256:') === 0) { - return imageName.substring(7, 19); - } - return _.split(imageName, '@sha256')[0]; - }; -}); + return ''; + }; + }) + .filter('availablenodecount', [ + 'ConstraintsHelper', + function (ConstraintsHelper) { + 'use strict'; + return function (nodes, service) { + var availableNodes = 0; + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (node.Availability === 'active' && node.Status === 'ready' && ConstraintsHelper.matchesServiceConstraints(service, node)) { + availableNodes++; + } + } + return availableNodes; + }; + }, + ]) + .filter('runningtaskscount', function () { + 'use strict'; + return function (tasks) { + var runningTasks = 0; + for (var i = 0; i < tasks.length; i++) { + var task = tasks[i]; + if (task.Status.State === 'running' && task.DesiredState === 'running') { + runningTasks++; + } + } + return runningTasks; + }; + }) + .filter('runningcontainers', function () { + 'use strict'; + return function runningContainersFilter(containers) { + return containers.filter(function (container) { + return container.State === 'running'; + }).length; + }; + }) + .filter('stoppedcontainers', function () { + 'use strict'; + return function stoppedContainersFilter(containers) { + return containers.filter(function (container) { + return container.State === 'exited'; + }).length; + }; + }) + .filter('healthycontainers', function () { + 'use strict'; + return function healthyContainersFilter(containers) { + return containers.filter(function (container) { + return container.Status === 'healthy'; + }).length; + }; + }) + .filter('unhealthycontainers', function () { + 'use strict'; + return function unhealthyContainersFilter(containers) { + return containers.filter(function (container) { + return container.Status === 'unhealthy'; + }).length; + }; + }) + .filter('imagestotalsize', function () { + 'use strict'; + return function (images) { + var totalImageSize = 0; + for (var i = 0; i < images.length; i++) { + var item = images[i]; + totalImageSize += item.VirtualSize; + } + return totalImageSize; + }; + }) + .filter('tasknodename', function () { + 'use strict'; + return function (nodeId, nodes) { + var node = _.find(nodes, { Id: nodeId }); + if (node) { + return node.Hostname; + } + return ''; + }; + }) + .filter('imagelayercommand', function () { + 'use strict'; + return function (createdBy) { + return createdBy.replace('/bin/sh -c #(nop) ', '').replace('/bin/sh -c ', 'RUN '); + }; + }) + .filter('trimshasum', function () { + 'use strict'; + return function (imageName) { + if (!imageName) { + return; + } + if (imageName.indexOf('sha256:') === 0) { + return imageName.substring(7, 19); + } + return _.split(imageName, '@sha256')[0]; + }; + }); diff --git a/app/docker/helpers/configHelper.js b/app/docker/helpers/configHelper.js index 104377414..00c1dbcde 100644 --- a/app/docker/helpers/configHelper.js +++ b/app/docker/helpers/configHelper.js @@ -1,34 +1,35 @@ -angular.module('portainer.docker') -.factory('ConfigHelper', [function ConfigHelperFactory() { - 'use strict'; - return { - flattenConfig: function(config) { - if (config) { - return { - Id: config.ConfigID, - Name: config.ConfigName, - FileName: config.File.Name, - Uid: config.File.UID, - Gid: config.File.GID, - Mode: config.File.Mode - }; - } - return {}; - }, - configConfig: function(config) { - if (config) { - return { - ConfigID: config.Id, - ConfigName: config.Name, - File: { - Name: config.FileName || config.Name, - UID: config.Uid || '0', - GID: config.Gid || '0', - Mode: config.Mode || 292 - } - }; - } - return {}; - } - }; -}]); +angular.module('portainer.docker').factory('ConfigHelper', [ + function ConfigHelperFactory() { + 'use strict'; + return { + flattenConfig: function (config) { + if (config) { + return { + Id: config.ConfigID, + Name: config.ConfigName, + FileName: config.File.Name, + Uid: config.File.UID, + Gid: config.File.GID, + Mode: config.File.Mode, + }; + } + return {}; + }, + configConfig: function (config) { + if (config) { + return { + ConfigID: config.Id, + ConfigName: config.Name, + File: { + Name: config.FileName || config.Name, + UID: config.Uid || '0', + GID: config.Gid || '0', + Mode: config.Mode || 292, + }, + }; + } + return {}; + }, + }; + }, +]); diff --git a/app/docker/helpers/constraintsHelper.js b/app/docker/helpers/constraintsHelper.js index f006dcf09..c2288f71b 100644 --- a/app/docker/helpers/constraintsHelper.js +++ b/app/docker/helpers/constraintsHelper.js @@ -12,18 +12,16 @@ var patterns = { nodeHostname: 'node.hostname', nodeRole: 'node.role', nodeLabels: 'node.labels.', - engineLabels: 'engine.labels.' + engineLabels: 'engine.labels.', }, op: { eq: '==', - neq: '!=' - } + neq: '!=', + }, }; function matchesConstraint(value, constraint) { - if (!constraint || - (constraint.op === patterns.op.eq && value === constraint.value) || - (constraint.op === patterns.op.neq && value !== constraint.value)) { + if (!constraint || (constraint.op === patterns.op.eq && value === constraint.value) || (constraint.op === patterns.op.neq && value !== constraint.value)) { return true; } return false; @@ -47,8 +45,8 @@ function extractCustomLabelKey(constraint, op, baseLabelKey) { return constraint.split(op).shift().trim().replace(baseLabelKey, ''); } -angular.module('portainer.docker') - .factory('ConstraintsHelper', [function ConstraintsHelperFactory() { +angular.module('portainer.docker').factory('ConstraintsHelper', [ + function ConstraintsHelperFactory() { 'use strict'; return { transformConstraints: function (constraints) { @@ -94,7 +92,8 @@ angular.module('portainer.docker') return true; } var constraints = this.transformConstraints(angular.copy(service.Constraints)); - if (matchesConstraint(node.Id, constraints.nodeId) && + if ( + matchesConstraint(node.Id, constraints.nodeId) && matchesConstraint(node.Hostname, constraints.nodeHostname) && matchesConstraint(node.Role, constraints.nodeRole) && matchesLabel(node.Labels, constraints.nodeLabels) && @@ -103,6 +102,7 @@ angular.module('portainer.docker') return true; } return false; - } + }, }; - }]); \ No newline at end of file + }, +]); diff --git a/app/docker/helpers/containerHelper.js b/app/docker/helpers/containerHelper.js index ab169a4c5..e047676b7 100644 --- a/app/docker/helpers/containerHelper.js +++ b/app/docker/helpers/containerHelper.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import splitargs from 'splitargs/src/splitargs' +import splitargs from 'splitargs/src/splitargs'; const portPattern = /^([1-9]|[1-5]?[0-9]{2,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/m; @@ -33,8 +33,7 @@ function isValidPortRange(portRange) { portRange = parsePortRange(); } - return Array.isArray(portRange) && portRange.length === 2 && - portRange[0] > 0 && portRange[1] >= portRange[0]; + return Array.isArray(portRange) && portRange.length === 2 && portRange[0] > 0 && portRange[1] >= portRange[0]; } function createPortRange(portRangeText, port) { @@ -49,7 +48,7 @@ function createPortRange(portRangeText, port) { portRangeText = portRangeText.substr(colonIndex + 1); } - port = (typeof port === 'number' ? port : parsePort(port)); + port = typeof port === 'number' ? port : parsePort(port); const portRange = parsePortRange(portRangeText); const startPort = Math.min(portRange[0], port); const endPort = Math.max(portRange[1], port); @@ -61,178 +60,181 @@ function createPortRange(portRangeText, port) { } } -angular.module('portainer.docker') -.factory('ContainerHelper', [function ContainerHelperFactory() { - 'use strict'; - var helper = {}; +angular.module('portainer.docker').factory('ContainerHelper', [ + function ContainerHelperFactory() { + 'use strict'; + var helper = {}; - helper.commandStringToArray = function(command) { - return splitargs(command); - }; - - helper.commandArrayToString = function(array) { - return array.map(function(elem) { - return '\'' + elem + '\''; - }).join(' '); - }; - - helper.configFromContainer = function(container) { - var config = container.Config; - // HostConfig - config.HostConfig = container.HostConfig; - // Name - config.name = container.Name.replace(/^\//g, ''); - // Network - var mode = config.HostConfig.NetworkMode; - config.NetworkingConfig = { - 'EndpointsConfig': {} + helper.commandStringToArray = function (command) { + return splitargs(command); }; - config.NetworkingConfig.EndpointsConfig = container.NetworkSettings.Networks; - if (mode.indexOf('container:') !== -1) { - delete config.Hostname; - delete config.ExposedPorts; - } - // Set volumes - var binds = []; - var volumes = {}; - for (var v in container.Mounts) { - if ({}.hasOwnProperty.call(container.Mounts, v)) { - var mount = container.Mounts[v]; - var name = mount.Name || mount.Source; - var containerPath = mount.Destination; - if (name && containerPath) { - var bind = name + ':' + containerPath; - volumes[containerPath] = {}; - if (mount.RW === false) { - bind += ':ro'; + + helper.commandArrayToString = function (array) { + return array + .map(function (elem) { + return "'" + elem + "'"; + }) + .join(' '); + }; + + helper.configFromContainer = function (container) { + var config = container.Config; + // HostConfig + config.HostConfig = container.HostConfig; + // Name + config.name = container.Name.replace(/^\//g, ''); + // Network + var mode = config.HostConfig.NetworkMode; + config.NetworkingConfig = { + EndpointsConfig: {}, + }; + config.NetworkingConfig.EndpointsConfig = container.NetworkSettings.Networks; + if (mode.indexOf('container:') !== -1) { + delete config.Hostname; + delete config.ExposedPorts; + } + // Set volumes + var binds = []; + var volumes = {}; + for (var v in container.Mounts) { + if ({}.hasOwnProperty.call(container.Mounts, v)) { + var mount = container.Mounts[v]; + var name = mount.Name || mount.Source; + var containerPath = mount.Destination; + if (name && containerPath) { + var bind = name + ':' + containerPath; + volumes[containerPath] = {}; + if (mount.RW === false) { + bind += ':ro'; + } + binds.push(bind); } - binds.push(bind); } } - } - config.HostConfig.Binds = binds; - config.Volumes = volumes; - return config; - }; + config.HostConfig.Binds = binds; + config.Volumes = volumes; + return config; + }; - helper.preparePortBindings = function(portBindings) { - const bindings = {}; - _.forEach(portBindings, (portBinding) => { - if (!portBinding.containerPort) { - return; - } - - let hostPort = portBinding.hostPort; - const containerPortRange = parsePortRange(portBinding.containerPort); - if (!isValidPortRange(containerPortRange)) { - throw new Error('Invalid port specification: ' + portBinding.containerPort); - } - - const startPort = containerPortRange[0]; - const endPort = containerPortRange[1]; - let hostIp = undefined; - let startHostPort = 0; - let endHostPort = 0; - if (hostPort) { - if (hostPort.indexOf(':') > -1) { - const hostAndPort = _.split(hostPort, ':'); - hostIp = hostAndPort[0]; - hostPort = hostAndPort[1]; + helper.preparePortBindings = function (portBindings) { + const bindings = {}; + _.forEach(portBindings, (portBinding) => { + if (!portBinding.containerPort) { + return; } - const hostPortRange = parsePortRange(hostPort); - if (!isValidPortRange(hostPortRange)) { - throw new Error('Invalid port specification: ' + hostPort); + let hostPort = portBinding.hostPort; + const containerPortRange = parsePortRange(portBinding.containerPort); + if (!isValidPortRange(containerPortRange)) { + throw new Error('Invalid port specification: ' + portBinding.containerPort); } - startHostPort = hostPortRange[0]; - endHostPort = hostPortRange[1]; - if (endPort !== startPort && (endPort - startPort) !== (endHostPort - startHostPort)) { - throw new Error('Invalid port specification: ' + hostPort); - } - } + const startPort = containerPortRange[0]; + const endPort = containerPortRange[1]; + let hostIp = undefined; + let startHostPort = 0; + let endHostPort = 0; + if (hostPort) { + if (hostPort.indexOf(':') > -1) { + const hostAndPort = _.split(hostPort, ':'); + hostIp = hostAndPort[0]; + hostPort = hostAndPort[1]; + } - for (let i = 0; i <= (endPort - startPort); i++) { - const containerPort = (startPort + i).toString(); - if (startHostPort > 0) { - hostPort = (startHostPort + i).toString(); - } - if (startPort === endPort && startHostPort !== endHostPort) { - hostPort += '-' + endHostPort.toString(); + const hostPortRange = parsePortRange(hostPort); + if (!isValidPortRange(hostPortRange)) { + throw new Error('Invalid port specification: ' + hostPort); + } + + startHostPort = hostPortRange[0]; + endHostPort = hostPortRange[1]; + if (endPort !== startPort && endPort - startPort !== endHostPort - startHostPort) { + throw new Error('Invalid port specification: ' + hostPort); + } } - const bindKey = containerPort + '/' + portBinding.protocol; - bindings[bindKey] = [{ HostIp: hostIp, HostPort: hostPort }]; - } - }); - return bindings; - }; + for (let i = 0; i <= endPort - startPort; i++) { + const containerPort = (startPort + i).toString(); + if (startHostPort > 0) { + hostPort = (startHostPort + i).toString(); + } + if (startPort === endPort && startHostPort !== endHostPort) { + hostPort += '-' + endHostPort.toString(); + } - helper.sortAndCombinePorts = function(portBindings) { - const bindings = []; - const portBindingKeys = _.keys(portBindings); + const bindKey = containerPort + '/' + portBinding.protocol; + bindings[bindKey] = [{ HostIp: hostIp, HostPort: hostPort }]; + } + }); + return bindings; + }; - // Group the port bindings by protocol - const portBindingKeysByProtocol = _.groupBy(portBindingKeys, (portKey) => { - return _.split(portKey, '/')[1]; - }); + helper.sortAndCombinePorts = function (portBindings) { + const bindings = []; + const portBindingKeys = _.keys(portBindings); - _.forEach(portBindingKeysByProtocol, (portBindingKeys, protocol) => { - // Group the port bindings by host IP - const portBindingKeysByHostIp = _.groupBy(portBindingKeys, (portKey) => { - const portBinding = portBindings[portKey][0]; - return portBinding.HostIp || ''; + // Group the port bindings by protocol + const portBindingKeysByProtocol = _.groupBy(portBindingKeys, (portKey) => { + return _.split(portKey, '/')[1]; }); - _.forEach(portBindingKeysByHostIp, (portBindingKeys) => { - // Sort by host port - const sortedPortBindingKeys = _.orderBy(portBindingKeys, (portKey) => { - return parseInt(_.split(portKey, '/')[0]); + _.forEach(portBindingKeysByProtocol, (portBindingKeys, protocol) => { + // Group the port bindings by host IP + const portBindingKeysByHostIp = _.groupBy(portBindingKeys, (portKey) => { + const portBinding = portBindings[portKey][0]; + return portBinding.HostIp || ''; }); - let previousHostPort = -1; - let previousContainerPort = -1; - _.forEach(sortedPortBindingKeys, (portKey) => { - const portKeySplit = _.split(portKey, '/'); - const containerPort = parseInt(portKeySplit[0]); - const portBinding = portBindings[portKey][0]; - const hostPort = parsePort(portBinding.HostPort); + _.forEach(portBindingKeysByHostIp, (portBindingKeys) => { + // Sort by host port + const sortedPortBindingKeys = _.orderBy(portBindingKeys, (portKey) => { + return parseInt(_.split(portKey, '/')[0]); + }); + + let previousHostPort = -1; + let previousContainerPort = -1; + _.forEach(sortedPortBindingKeys, (portKey) => { + const portKeySplit = _.split(portKey, '/'); + const containerPort = parseInt(portKeySplit[0]); + const portBinding = portBindings[portKey][0]; + const hostPort = parsePort(portBinding.HostPort); + + // We only combine single ports, and skip the host port ranges on one container port + if (hostPort > 0) { + // If we detect consecutive ports, we create a range of them + if (bindings.length > 0 && previousHostPort === hostPort - 1 && previousContainerPort === containerPort - 1) { + bindings[bindings.length - 1].hostPort = createPortRange(bindings[bindings.length - 1].hostPort, hostPort); + bindings[bindings.length - 1].containerPort = createPortRange(bindings[bindings.length - 1].containerPort, containerPort); + previousHostPort = hostPort; + previousContainerPort = containerPort; + return; + } - // We only combine single ports, and skip the host port ranges on one container port - if (hostPort > 0) { - // If we detect consecutive ports, we create a range of them - if (bindings.length > 0 && previousHostPort === (hostPort - 1) && previousContainerPort === (containerPort - 1)) { - bindings[bindings.length-1].hostPort = createPortRange(bindings[bindings.length-1].hostPort, hostPort); - bindings[bindings.length-1].containerPort = createPortRange(bindings[bindings.length-1].containerPort, containerPort); previousHostPort = hostPort; previousContainerPort = containerPort; - return; + } else { + previousHostPort = -1; + previousContainerPort = -1; } - previousHostPort = hostPort; - previousContainerPort = containerPort; - } else { - previousHostPort = -1; - previousContainerPort = -1; - } + let bindingHostPort = portBinding.HostPort.toString(); + if (portBinding.HostIp) { + bindingHostPort = portBinding.HostIp + ':' + bindingHostPort; + } - let bindingHostPort = portBinding.HostPort.toString(); - if (portBinding.HostIp) { - bindingHostPort = portBinding.HostIp + ':' + bindingHostPort; - } - - const binding = { - hostPort: bindingHostPort, - containerPort: containerPort, - protocol: protocol - }; - bindings.push(binding); + const binding = { + hostPort: bindingHostPort, + containerPort: containerPort, + protocol: protocol, + }; + bindings.push(binding); + }); }); }); - }); - return bindings; - }; + return bindings; + }; - return helper; -}]); + return helper; + }, +]); diff --git a/app/docker/helpers/imageHelper.js b/app/docker/helpers/imageHelper.js index b1bd8d78d..ea28976de 100644 --- a/app/docker/helpers/imageHelper.js +++ b/app/docker/helpers/imageHelper.js @@ -1,72 +1,73 @@ import _ from 'lodash-es'; import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes'; -angular.module('portainer.docker') -.factory('ImageHelper', [function ImageHelperFactory() { - 'use strict'; +angular.module('portainer.docker').factory('ImageHelper', [ + function ImageHelperFactory() { + 'use strict'; - var helper = {}; + var helper = {}; - helper.isValidTag = isValidTag; - helper.createImageConfigForContainer = createImageConfigForContainer; - helper.getImagesNamesForDownload = getImagesNamesForDownload; - helper.removeDigestFromRepository = removeDigestFromRepository; - helper.imageContainsURL = imageContainsURL; + helper.isValidTag = isValidTag; + helper.createImageConfigForContainer = createImageConfigForContainer; + helper.getImagesNamesForDownload = getImagesNamesForDownload; + helper.removeDigestFromRepository = removeDigestFromRepository; + helper.imageContainsURL = imageContainsURL; - function isValidTag(tag) { - return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g); - } + function isValidTag(tag) { + return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g); + } - function getImagesNamesForDownload(images) { - var names = images.map(function(image) { - return image.RepoTags[0] !== ':' ? image.RepoTags[0] : image.Id; - }); - return { - names: names - }; - } + function getImagesNamesForDownload(images) { + var names = images.map(function (image) { + return image.RepoTags[0] !== ':' ? image.RepoTags[0] : image.Id; + }); + return { + names: names, + }; + } - /** - * - * @param {PorImageRegistryModel} registry - */ - function createImageConfigForContainer(registry) { - const data = { - fromImage: '' - }; - let fullImageName = ''; + /** + * + * @param {PorImageRegistryModel} registry + */ + function createImageConfigForContainer(registry) { + const data = { + fromImage: '', + }; + let fullImageName = ''; - if (registry.UseRegistry) { - if (registry.Registry.Type === RegistryTypes.GITLAB) { - const slash = _.startsWith(registry.Image, ':') ? '' : '/'; - fullImageName = registry.Registry.URL + '/' + registry.Registry.Gitlab.ProjectPath + slash + registry.Image; + if (registry.UseRegistry) { + if (registry.Registry.Type === RegistryTypes.GITLAB) { + const slash = _.startsWith(registry.Image, ':') ? '' : '/'; + fullImageName = registry.Registry.URL + '/' + registry.Registry.Gitlab.ProjectPath + slash + registry.Image; + } else { + const url = registry.Registry.URL ? registry.Registry.URL + '/' : ''; + fullImageName = url + registry.Image; + } + if (!_.includes(registry.Image, ':')) { + fullImageName += ':latest'; + } } else { - const url = registry.Registry.URL ? registry.Registry.URL + '/' : ''; - fullImageName = url + registry.Image; + fullImageName = registry.Image; } - if (!_.includes(registry.Image, ':')) { - fullImageName += ':latest'; - } - } else { - fullImageName = registry.Image; + + data.fromImage = fullImageName; + return data; } - data.fromImage = fullImageName; - return data; - } - - function imageContainsURL(image) { - const split = _.split(image, '/'); - const url = split[0]; - if (split.length > 1) { - return _.includes(url, '.') || _.includes(url, ':'); + function imageContainsURL(image) { + const split = _.split(image, '/'); + const url = split[0]; + if (split.length > 1) { + return _.includes(url, '.') || _.includes(url, ':'); + } + return false; } - return false; - } - function removeDigestFromRepository(repository) { - return repository.split('@sha')[0]; - } + function removeDigestFromRepository(repository) { + return repository.split('@sha')[0]; + } - return helper; -}]); + return helper; + }, +]); diff --git a/app/docker/helpers/infoHelper.js b/app/docker/helpers/infoHelper.js index 05f00012e..3ec824793 100644 --- a/app/docker/helpers/infoHelper.js +++ b/app/docker/helpers/infoHelper.js @@ -1,38 +1,39 @@ import _ from 'lodash-es'; -angular.module('portainer.docker') -.factory('InfoHelper', [function InfoHelperFactory() { - 'use strict'; +angular.module('portainer.docker').factory('InfoHelper', [ + function InfoHelperFactory() { + 'use strict'; - var helper = {}; + var helper = {}; - helper.determineEndpointMode = function(info, type) { - var mode = { - provider: '', - role: '', - agentProxy: false + helper.determineEndpointMode = function (info, type) { + var mode = { + provider: '', + role: '', + agentProxy: false, + }; + + if (type === 2 || type === 4) { + mode.agentProxy = true; + } + + if (!info.Swarm || _.isEmpty(info.Swarm.NodeID)) { + if (info.ID === 'vSphere Integrated Containers') { + mode.provider = 'VMWARE_VIC'; + } else { + mode.provider = 'DOCKER_STANDALONE'; + } + } else { + mode.provider = 'DOCKER_SWARM_MODE'; + if (info.Swarm.ControlAvailable) { + mode.role = 'MANAGER'; + } else { + mode.role = 'WORKER'; + } + } + return mode; }; - if (type === 2 || type === 4) { - mode.agentProxy = true; - } - - if (!info.Swarm || _.isEmpty(info.Swarm.NodeID)) { - if (info.ID === 'vSphere Integrated Containers') { - mode.provider = 'VMWARE_VIC'; - } else { - mode.provider = 'DOCKER_STANDALONE'; - } - } else { - mode.provider = 'DOCKER_SWARM_MODE'; - if (info.Swarm.ControlAvailable) { - mode.role = 'MANAGER'; - } else { - mode.role = 'WORKER'; - } - } - return mode; - }; - - return helper; -}]); + return helper; + }, +]); diff --git a/app/docker/helpers/labelHelper.js b/app/docker/helpers/labelHelper.js index 85de78ab1..b19c552cb 100644 --- a/app/docker/helpers/labelHelper.js +++ b/app/docker/helpers/labelHelper.js @@ -1,25 +1,26 @@ -angular.module('portainer.docker') -.factory('LabelHelper', [function LabelHelperFactory() { - 'use strict'; - return { - fromLabelHashToKeyValue: function(labels) { - if (labels) { - return Object.keys(labels).map(function(key) { - return {key: key, value: labels[key], originalKey: key, originalValue: labels[key], added: true}; - }); - } - return []; - }, - fromKeyValueToLabelHash: function(labelKV) { - var labels = {}; - if (labelKV) { - labelKV.forEach(function(label) { - if (label.key) { - labels[label.key] = label.value; - } - }); - } - return labels; - } - }; -}]); +angular.module('portainer.docker').factory('LabelHelper', [ + function LabelHelperFactory() { + 'use strict'; + return { + fromLabelHashToKeyValue: function (labels) { + if (labels) { + return Object.keys(labels).map(function (key) { + return { key: key, value: labels[key], originalKey: key, originalValue: labels[key], added: true }; + }); + } + return []; + }, + fromKeyValueToLabelHash: function (labelKV) { + var labels = {}; + if (labelKV) { + labelKV.forEach(function (label) { + if (label.key) { + labels[label.key] = label.value; + } + }); + } + return labels; + }, + }; + }, +]); diff --git a/app/docker/helpers/logHelper.js b/app/docker/helpers/logHelper.js index 47f903056..052a69843 100644 --- a/app/docker/helpers/logHelper.js +++ b/app/docker/helpers/logHelper.js @@ -1,22 +1,22 @@ -angular.module('portainer.docker') -.factory('LogHelper', [function LogHelperFactory() { - 'use strict'; - var helper = {}; +angular.module('portainer.docker').factory('LogHelper', [ + function LogHelperFactory() { + 'use strict'; + var helper = {}; - // Return an array with each line being an entry. - // It will also remove any ANSI code related character sequences. - // If the skipHeaders param is specified, it will strip the 8 first characters of each line. - helper.formatLogs = function(logs, skipHeaders) { - logs = logs.replace( - /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); + // Return an array with each line being an entry. + // It will also remove any ANSI code related character sequences. + // If the skipHeaders param is specified, it will strip the 8 first characters of each line. + helper.formatLogs = function (logs, skipHeaders) { + logs = logs.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); - if (skipHeaders) { - logs = logs.substring(8); - logs = logs.replace(/\n(.{8})/g, '\n\r'); - } + if (skipHeaders) { + logs = logs.substring(8); + logs = logs.replace(/\n(.{8})/g, '\n\r'); + } - return logs.split('\n'); - }; + return logs.split('\n'); + }; - return helper; -}]); + return helper; + }, +]); diff --git a/app/docker/helpers/nodeHelper.js b/app/docker/helpers/nodeHelper.js index ba02dfdd1..a97a7b81c 100644 --- a/app/docker/helpers/nodeHelper.js +++ b/app/docker/helpers/nodeHelper.js @@ -1,14 +1,15 @@ -angular.module('portainer.docker') -.factory('NodeHelper', [function NodeHelperFactory() { - 'use strict'; - return { - nodeToConfig: function(node) { - return { - Name: node.Spec.Name, - Role: node.Spec.Role, - Labels: node.Spec.Labels, - Availability: node.Spec.Availability - }; - } - }; -}]); +angular.module('portainer.docker').factory('NodeHelper', [ + function NodeHelperFactory() { + 'use strict'; + return { + nodeToConfig: function (node) { + return { + Name: node.Spec.Name, + Role: node.Spec.Role, + Labels: node.Spec.Labels, + Availability: node.Spec.Availability, + }; + }, + }; + }, +]); diff --git a/app/docker/helpers/secretHelper.js b/app/docker/helpers/secretHelper.js index a96241474..9948f8481 100644 --- a/app/docker/helpers/secretHelper.js +++ b/app/docker/helpers/secretHelper.js @@ -1,34 +1,35 @@ -angular.module('portainer.docker') -.factory('SecretHelper', [function SecretHelperFactory() { - 'use strict'; - return { - flattenSecret: function(secret) { - if (secret) { - return { - Id: secret.SecretID, - Name: secret.SecretName, - FileName: secret.File.Name, - Uid: secret.File.UID, - Gid: secret.File.GID, - Mode: secret.File.Mode - }; - } - return {}; - }, - secretConfig: function(secret) { - if (secret) { - return { - SecretID: secret.Id, - SecretName: secret.Name, - File: { - Name: secret.FileName, - UID: secret.Uid || '0', - GID: secret.Gid || '0', - Mode: secret.Mode || 444 - } - }; - } - return {}; - } - }; -}]); +angular.module('portainer.docker').factory('SecretHelper', [ + function SecretHelperFactory() { + 'use strict'; + return { + flattenSecret: function (secret) { + if (secret) { + return { + Id: secret.SecretID, + Name: secret.SecretName, + FileName: secret.File.Name, + Uid: secret.File.UID, + Gid: secret.File.GID, + Mode: secret.File.Mode, + }; + } + return {}; + }, + secretConfig: function (secret) { + if (secret) { + return { + SecretID: secret.Id, + SecretName: secret.Name, + File: { + Name: secret.FileName, + UID: secret.Uid || '0', + GID: secret.Gid || '0', + Mode: secret.Mode || 444, + }, + }; + } + return {}; + }, + }; + }, +]); diff --git a/app/docker/helpers/serviceHelper.js b/app/docker/helpers/serviceHelper.js index f9d2fb689..8788c2c74 100644 --- a/app/docker/helpers/serviceHelper.js +++ b/app/docker/helpers/serviceHelper.js @@ -1,250 +1,251 @@ import moment from 'moment'; -angular.module('portainer.docker') -.factory('ServiceHelper', [function ServiceHelperFactory() { - 'use strict'; +angular.module('portainer.docker').factory('ServiceHelper', [ + function ServiceHelperFactory() { + 'use strict'; - var helper = {}; + var helper = {}; - helper.associateTasksToService = function(service, tasks) { - service.Tasks = []; - var otherServicesTasks = []; - for (var i = 0; i < tasks.length; i++) { - var task = tasks[i]; - if (task.ServiceId === service.Id) { - service.Tasks.push(task); - task.ServiceName = service.Name; - } else { - otherServicesTasks.push(task); + helper.associateTasksToService = function (service, tasks) { + service.Tasks = []; + var otherServicesTasks = []; + for (var i = 0; i < tasks.length; i++) { + var task = tasks[i]; + if (task.ServiceId === service.Id) { + service.Tasks.push(task); + task.ServiceName = service.Name; + } else { + otherServicesTasks.push(task); + } } - } - tasks = otherServicesTasks; - }; - - helper.serviceToConfig = function(service) { - return { - Name: service.Spec.Name, - Labels: service.Spec.Labels, - TaskTemplate: service.Spec.TaskTemplate, - Mode: service.Spec.Mode, - UpdateConfig: service.Spec.UpdateConfig, - Networks: service.Spec.Networks, - EndpointSpec: service.Spec.EndpointSpec + tasks = otherServicesTasks; }; - }; - helper.translateKeyValueToPlacementPreferences = function(keyValuePreferences) { - if (keyValuePreferences) { - var preferences = []; - keyValuePreferences.forEach(function(preference) { - if (preference.strategy && preference.strategy !== '' && preference.value && preference.value !== '') { - switch (preference.strategy.toLowerCase()) { - case 'spread': - preferences.push({ - 'Spread': { - 'SpreadDescriptor': preference.value - } - }); - break; + helper.serviceToConfig = function (service) { + return { + Name: service.Spec.Name, + Labels: service.Spec.Labels, + TaskTemplate: service.Spec.TaskTemplate, + Mode: service.Spec.Mode, + UpdateConfig: service.Spec.UpdateConfig, + Networks: service.Spec.Networks, + EndpointSpec: service.Spec.EndpointSpec, + }; + }; + + helper.translateKeyValueToPlacementPreferences = function (keyValuePreferences) { + if (keyValuePreferences) { + var preferences = []; + keyValuePreferences.forEach(function (preference) { + if (preference.strategy && preference.strategy !== '' && preference.value && preference.value !== '') { + switch (preference.strategy.toLowerCase()) { + case 'spread': + preferences.push({ + Spread: { + SpreadDescriptor: preference.value, + }, + }); + break; + } } - } - }); - return preferences; - } - return []; - }; - - helper.translateKeyValueToPlacementConstraints = function(keyValueConstraints) { - if (keyValueConstraints) { - var constraints = []; - keyValueConstraints.forEach(function(constraint) { - if (constraint.key && constraint.key !== '' && constraint.value && constraint.value !== '') { - constraints.push(constraint.key + constraint.operator + constraint.value); - } - }); - return constraints; - } - return []; - }; - - helper.translateEnvironmentVariables = function(env) { - if (env) { - var variables = []; - env.forEach(function(variable) { - var idx = variable.indexOf('='); - var keyValue = [variable.slice(0, idx), variable.slice(idx + 1)]; - var originalValue = (keyValue.length > 1) ? keyValue[1] : ''; - variables.push({ - key: keyValue[0], - value: originalValue, - originalKey: keyValue[0], - originalValue: originalValue, - added: true }); - }); - return variables; - } - return []; - }; + return preferences; + } + return []; + }; - helper.translateEnvironmentVariablesToEnv = function(env) { - if (env) { - var variables = []; - env.forEach(function(variable) { - if (variable.key && variable.key !== '') { - variables.push(variable.key + '=' + variable.value); - } - }); - return variables; - } - return []; - }; + helper.translateKeyValueToPlacementConstraints = function (keyValueConstraints) { + if (keyValueConstraints) { + var constraints = []; + keyValueConstraints.forEach(function (constraint) { + if (constraint.key && constraint.key !== '' && constraint.value && constraint.value !== '') { + constraints.push(constraint.key + constraint.operator + constraint.value); + } + }); + return constraints; + } + return []; + }; - helper.translatePreferencesToKeyValue = function(preferences) { - if (preferences) { - var keyValuePreferences = []; - preferences.forEach(function(preference) { - if (preference.Spread) { - keyValuePreferences.push({ - strategy: 'Spread', - value: preference.Spread.SpreadDescriptor + helper.translateEnvironmentVariables = function (env) { + if (env) { + var variables = []; + env.forEach(function (variable) { + var idx = variable.indexOf('='); + var keyValue = [variable.slice(0, idx), variable.slice(idx + 1)]; + var originalValue = keyValue.length > 1 ? keyValue[1] : ''; + variables.push({ + key: keyValue[0], + value: originalValue, + originalKey: keyValue[0], + originalValue: originalValue, + added: true, }); - } - }); - return keyValuePreferences; - } - return []; - }; - - helper.translateConstraintsToKeyValue = function(constraints) { - function getOperator(constraint) { - var indexEquals = constraint.indexOf('=='); - if (indexEquals >= 0) { - return [indexEquals, '==']; - } - return [constraint.indexOf('!='), '!=']; - } - if (constraints) { - var keyValueConstraints = []; - constraints.forEach(function(constraint) { - var operatorIndices = getOperator(constraint); - - var key = constraint.slice(0, operatorIndices[0]); - var operator = operatorIndices[1]; - var value = constraint.slice(operatorIndices[0] + 2); - - keyValueConstraints.push({ - key: key, - value: value, - operator: operator, - originalKey: key, - originalValue: value }); - }); - return keyValueConstraints; - } - }; - - helper.translateHumanDurationToNanos = function(humanDuration) { - var nanos; - var regex = /^([0-9]+)(h|m|s|ms|us|ns)$/i; - var matches = humanDuration.match(regex); - - if (matches !== null && matches.length === 3) { - var duration = parseInt(matches[1], 10); - var unit = matches[2]; - // Moment.js cannot use micro or nanoseconds - switch (unit) { - case 'ns': - nanos = duration; - break; - case 'us': - nanos = duration * 1000; - break; - default: - nanos = moment.duration(duration, unit).asMilliseconds() * 1000000; + return variables; } - } - return nanos; - }; + return []; + }; - // Convert nanoseconds to the higher unit possible - // e.g 1840 nanoseconds = 1804ns - // e.g 300000000000 nanoseconds = 5m - // e.g 3510000000000 nanoseconds = 3510s - // e.g 3540000000000 nanoseconds = 59m - // e.g 3600000000000 nanoseconds = 1h - - helper.translateNanosToHumanDuration = function(nanos) { - var humanDuration = '0s'; - var conversionFromNano = {}; - conversionFromNano['ns'] = 1; - conversionFromNano['us'] = conversionFromNano['ns'] * 1000; - conversionFromNano['ms'] = conversionFromNano['us'] * 1000; - conversionFromNano['s'] = conversionFromNano['ms'] * 1000; - conversionFromNano['m'] = conversionFromNano['s'] * 60; - conversionFromNano['h'] = conversionFromNano['m'] * 60; - - Object.keys(conversionFromNano).forEach(function(unit) { - if ( nanos % conversionFromNano[unit] === 0 && (nanos / conversionFromNano[unit]) > 0) { - humanDuration = (nanos / conversionFromNano[unit]) + unit; - } - }); - return humanDuration; - }; - - helper.translateLogDriverOptsToKeyValue = function(logOptions) { - var options = []; - if (logOptions) { - Object.keys(logOptions).forEach(function(key) { - options.push({ - key: key, - value: logOptions[key], - originalKey: key, - originalValue: logOptions[key], - added: true + helper.translateEnvironmentVariablesToEnv = function (env) { + if (env) { + var variables = []; + env.forEach(function (variable) { + if (variable.key && variable.key !== '') { + variables.push(variable.key + '=' + variable.value); + } }); - }); - } - return options; - }; + return variables; + } + return []; + }; - helper.translateKeyValueToLogDriverOpts = function(keyValueLogDriverOpts) { - var options = {}; - if (keyValueLogDriverOpts) { - keyValueLogDriverOpts.forEach(function(option) { - if (option.key && option.key !== '' && option.value && option.value !== '') { - options[option.key] = option.value; + helper.translatePreferencesToKeyValue = function (preferences) { + if (preferences) { + var keyValuePreferences = []; + preferences.forEach(function (preference) { + if (preference.Spread) { + keyValuePreferences.push({ + strategy: 'Spread', + value: preference.Spread.SpreadDescriptor, + }); + } + }); + return keyValuePreferences; + } + return []; + }; + + helper.translateConstraintsToKeyValue = function (constraints) { + function getOperator(constraint) { + var indexEquals = constraint.indexOf('=='); + if (indexEquals >= 0) { + return [indexEquals, '==']; + } + return [constraint.indexOf('!='), '!=']; + } + if (constraints) { + var keyValueConstraints = []; + constraints.forEach(function (constraint) { + var operatorIndices = getOperator(constraint); + + var key = constraint.slice(0, operatorIndices[0]); + var operator = operatorIndices[1]; + var value = constraint.slice(operatorIndices[0] + 2); + + keyValueConstraints.push({ + key: key, + value: value, + operator: operator, + originalKey: key, + originalValue: value, + }); + }); + return keyValueConstraints; + } + }; + + helper.translateHumanDurationToNanos = function (humanDuration) { + var nanos; + var regex = /^([0-9]+)(h|m|s|ms|us|ns)$/i; + var matches = humanDuration.match(regex); + + if (matches !== null && matches.length === 3) { + var duration = parseInt(matches[1], 10); + var unit = matches[2]; + // Moment.js cannot use micro or nanoseconds + switch (unit) { + case 'ns': + nanos = duration; + break; + case 'us': + nanos = duration * 1000; + break; + default: + nanos = moment.duration(duration, unit).asMilliseconds() * 1000000; + } + } + return nanos; + }; + + // Convert nanoseconds to the higher unit possible + // e.g 1840 nanoseconds = 1804ns + // e.g 300000000000 nanoseconds = 5m + // e.g 3510000000000 nanoseconds = 3510s + // e.g 3540000000000 nanoseconds = 59m + // e.g 3600000000000 nanoseconds = 1h + + helper.translateNanosToHumanDuration = function (nanos) { + var humanDuration = '0s'; + var conversionFromNano = {}; + conversionFromNano['ns'] = 1; + conversionFromNano['us'] = conversionFromNano['ns'] * 1000; + conversionFromNano['ms'] = conversionFromNano['us'] * 1000; + conversionFromNano['s'] = conversionFromNano['ms'] * 1000; + conversionFromNano['m'] = conversionFromNano['s'] * 60; + conversionFromNano['h'] = conversionFromNano['m'] * 60; + + Object.keys(conversionFromNano).forEach(function (unit) { + if (nanos % conversionFromNano[unit] === 0 && nanos / conversionFromNano[unit] > 0) { + humanDuration = nanos / conversionFromNano[unit] + unit; } }); - } - return options; - }; + return humanDuration; + }; - helper.translateHostsEntriesToHostnameIP = function(entries) { - var ipHostEntries = []; - if (entries) { - entries.forEach(function(entry) { - if (entry.indexOf(' ') && entry.split(' ').length === 2) { - var keyValue = entry.split(' '); - ipHostEntries.push({ hostname: keyValue[1], ip: keyValue[0]}); - } - }); - } - return ipHostEntries; - }; + helper.translateLogDriverOptsToKeyValue = function (logOptions) { + var options = []; + if (logOptions) { + Object.keys(logOptions).forEach(function (key) { + options.push({ + key: key, + value: logOptions[key], + originalKey: key, + originalValue: logOptions[key], + added: true, + }); + }); + } + return options; + }; - helper.translateHostnameIPToHostsEntries = function(entries) { - var ipHostEntries = []; - if (entries) { - entries.forEach(function(entry) { - if (entry.ip && entry.hostname) { - ipHostEntries.push(entry.ip + ' ' + entry.hostname); - } - }); - } - return ipHostEntries; - }; + helper.translateKeyValueToLogDriverOpts = function (keyValueLogDriverOpts) { + var options = {}; + if (keyValueLogDriverOpts) { + keyValueLogDriverOpts.forEach(function (option) { + if (option.key && option.key !== '' && option.value && option.value !== '') { + options[option.key] = option.value; + } + }); + } + return options; + }; - return helper; -}]); + helper.translateHostsEntriesToHostnameIP = function (entries) { + var ipHostEntries = []; + if (entries) { + entries.forEach(function (entry) { + if (entry.indexOf(' ') && entry.split(' ').length === 2) { + var keyValue = entry.split(' '); + ipHostEntries.push({ hostname: keyValue[1], ip: keyValue[0] }); + } + }); + } + return ipHostEntries; + }; + + helper.translateHostnameIPToHostsEntries = function (entries) { + var ipHostEntries = []; + if (entries) { + entries.forEach(function (entry) { + if (entry.ip && entry.hostname) { + ipHostEntries.push(entry.ip + ' ' + entry.hostname); + } + }); + } + return ipHostEntries; + }; + + return helper; + }, +]); diff --git a/app/docker/helpers/taskHelper.js b/app/docker/helpers/taskHelper.js index 5526c3c32..2a134318b 100644 --- a/app/docker/helpers/taskHelper.js +++ b/app/docker/helpers/taskHelper.js @@ -1,18 +1,19 @@ -angular.module('portainer.docker') -.factory('TaskHelper', [function TaskHelperFactory() { - 'use strict'; +angular.module('portainer.docker').factory('TaskHelper', [ + function TaskHelperFactory() { + 'use strict'; - var helper = {}; + var helper = {}; - helper.associateContainerToTask = function(task, containers) { - for (var i = 0; i < containers.length; i++) { - var container = containers[i]; - if (task.ContainerId === container.Id) { - task.Container = container; - break; + helper.associateContainerToTask = function (task, containers) { + for (var i = 0; i < containers.length; i++) { + var container = containers[i]; + if (task.ContainerId === container.Id) { + task.Container = container; + break; + } } - } - }; + }; - return helper; -}]); + return helper; + }, +]); diff --git a/app/docker/helpers/volumeHelper.js b/app/docker/helpers/volumeHelper.js index 20cc66c2d..58f735397 100644 --- a/app/docker/helpers/volumeHelper.js +++ b/app/docker/helpers/volumeHelper.js @@ -1,30 +1,31 @@ -angular.module('portainer.docker') -.factory('VolumeHelper', [function VolumeHelperFactory() { - 'use strict'; - var helper = {}; +angular.module('portainer.docker').factory('VolumeHelper', [ + function VolumeHelperFactory() { + 'use strict'; + var helper = {}; - helper.createDriverOptions = function(optionArray) { - var options = {}; - optionArray.forEach(function (option) { - options[option.name] = option.value; - }); - return options; - }; + helper.createDriverOptions = function (optionArray) { + var options = {}; + optionArray.forEach(function (option) { + options[option.name] = option.value; + }); + return options; + }; - helper.isVolumeUsedByAService = function(volume, services) { - for (var i = 0; i < services.length; i++) { - var service = services[i]; - var mounts = service.Mounts; - for (var j = 0; j < mounts.length; j++) { - var mount = mounts[j]; - if (mount.Source === volume.Id) { - return true; + helper.isVolumeUsedByAService = function (volume, services) { + for (var i = 0; i < services.length; i++) { + var service = services[i]; + var mounts = service.Mounts; + for (var j = 0; j < mounts.length; j++) { + var mount = mounts[j]; + if (mount.Source === volume.Id) { + return true; + } } } - } - return false; - }; + return false; + }; - return helper; -}]); + return helper; + }, +]); diff --git a/app/docker/interceptors/containersInterceptor.js b/app/docker/interceptors/containersInterceptor.js index 066ffe4fe..b6a9b340d 100644 --- a/app/docker/interceptors/containersInterceptor.js +++ b/app/docker/interceptors/containersInterceptor.js @@ -1,5 +1,7 @@ -angular.module('portainer.app') - .factory('ContainersInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) { +angular.module('portainer.app').factory('ContainersInterceptor', [ + '$q', + 'EndpointProvider', + function ($q, EndpointProvider) { 'use strict'; var interceptor = {}; @@ -18,4 +20,5 @@ angular.module('portainer.app') return $q.reject(rejection); } return interceptor; - }]); \ No newline at end of file + }, +]); diff --git a/app/docker/interceptors/imagesInterceptor.js b/app/docker/interceptors/imagesInterceptor.js index 9d79fbc86..5294d0b38 100644 --- a/app/docker/interceptors/imagesInterceptor.js +++ b/app/docker/interceptors/imagesInterceptor.js @@ -1,5 +1,7 @@ -angular.module('portainer.app') - .factory('ImagesInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) { +angular.module('portainer.app').factory('ImagesInterceptor', [ + '$q', + 'EndpointProvider', + function ($q, EndpointProvider) { 'use strict'; var interceptor = {}; @@ -18,4 +20,5 @@ angular.module('portainer.app') return $q.reject(rejection); } return interceptor; - }]); \ No newline at end of file + }, +]); diff --git a/app/docker/interceptors/infoInterceptor.js b/app/docker/interceptors/infoInterceptor.js index 17f310641..d7b6e97de 100644 --- a/app/docker/interceptors/infoInterceptor.js +++ b/app/docker/interceptors/infoInterceptor.js @@ -1,5 +1,7 @@ -angular.module('portainer.app') - .factory('InfoInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) { +angular.module('portainer.app').factory('InfoInterceptor', [ + '$q', + 'EndpointProvider', + function ($q, EndpointProvider) { 'use strict'; var interceptor = {}; @@ -18,4 +20,5 @@ angular.module('portainer.app') return $q.reject(rejection); } return interceptor; - }]); \ No newline at end of file + }, +]); diff --git a/app/docker/interceptors/networksInterceptor.js b/app/docker/interceptors/networksInterceptor.js index b5068534c..b3cfdce19 100644 --- a/app/docker/interceptors/networksInterceptor.js +++ b/app/docker/interceptors/networksInterceptor.js @@ -1,5 +1,7 @@ -angular.module('portainer.app') - .factory('NetworksInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) { +angular.module('portainer.app').factory('NetworksInterceptor', [ + '$q', + 'EndpointProvider', + function ($q, EndpointProvider) { 'use strict'; var interceptor = {}; @@ -18,4 +20,5 @@ angular.module('portainer.app') return $q.reject(rejection); } return interceptor; - }]); \ No newline at end of file + }, +]); diff --git a/app/docker/interceptors/versionInterceptor.js b/app/docker/interceptors/versionInterceptor.js index 95a5d56c4..019a34c52 100644 --- a/app/docker/interceptors/versionInterceptor.js +++ b/app/docker/interceptors/versionInterceptor.js @@ -1,5 +1,7 @@ -angular.module('portainer.app') - .factory('VersionInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) { +angular.module('portainer.app').factory('VersionInterceptor', [ + '$q', + 'EndpointProvider', + function ($q, EndpointProvider) { 'use strict'; var interceptor = {}; @@ -18,4 +20,5 @@ angular.module('portainer.app') return $q.reject(rejection); } return interceptor; - }]); + }, +]); diff --git a/app/docker/interceptors/volumesInterceptor.js b/app/docker/interceptors/volumesInterceptor.js index a42addd2c..bbb1dae35 100644 --- a/app/docker/interceptors/volumesInterceptor.js +++ b/app/docker/interceptors/volumesInterceptor.js @@ -1,5 +1,7 @@ -angular.module('portainer.app') - .factory('VolumesInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) { +angular.module('portainer.app').factory('VolumesInterceptor', [ + '$q', + 'EndpointProvider', + function ($q, EndpointProvider) { 'use strict'; var interceptor = {}; @@ -18,4 +20,5 @@ angular.module('portainer.app') return $q.reject(rejection); } return interceptor; - }]); \ No newline at end of file + }, +]); diff --git a/app/docker/models/config.js b/app/docker/models/config.js index e9b2db69f..0939c692b 100644 --- a/app/docker/models/config.js +++ b/app/docker/models/config.js @@ -1,9 +1,14 @@ import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; function b64DecodeUnicode(str) { - return decodeURIComponent(atob(str).split('').map(function(c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join('')); + return decodeURIComponent( + atob(str) + .split('') + .map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join('') + ); } export function ConfigViewModel(data) { diff --git a/app/docker/models/container.js b/app/docker/models/container.js index c09ad7748..58cc51a20 100644 --- a/app/docker/models/container.js +++ b/app/docker/models/container.js @@ -68,12 +68,14 @@ export function ContainerViewModel(data) { export function ContainerStatsViewModel(data) { this.read = data.read; this.preread = data.preread; - if(data.memory_stats.privateworkingset !== undefined) { // Windows + if (data.memory_stats.privateworkingset !== undefined) { + // Windows this.MemoryUsage = data.memory_stats.privateworkingset; this.MemoryCache = 0; this.NumProcs = data.num_procs; this.isWindows = true; - } else { // Linux + } else { + // Linux if (data.memory_stats.stats === undefined || data.memory_stats.usage === undefined) { this.MemoryUsage = this.MemoryCache = 0; } else { diff --git a/app/docker/models/containerCapabilities.js b/app/docker/models/containerCapabilities.js index 61706cc51..dadf14eac 100644 --- a/app/docker/models/containerCapabilities.js +++ b/app/docker/models/containerCapabilities.js @@ -1,90 +1,90 @@ var capDesc = { - 'SETPCAP': 'Modify process capabilities.', - 'MKNOD': 'Create special files using mknod(2).', - 'AUDIT_WRITE': 'Write records to kernel auditing log.', - 'CHOWN': 'Make arbitrary changes to file UIDs and GIDs (see chown(2)).', - 'NET_RAW': 'Use RAW and PACKET sockets.', - 'DAC_OVERRIDE': 'Bypass file read, write, and execute permission checks.', - 'FOWNER': 'Bypass permission checks on operations that normally require the file system UID of the process to match the UID of the file.', - 'FSETID': 'Don’t clear set-user-ID and set-group-ID permission bits when a file is modified.', - 'KILL': 'Bypass permission checks for sending signals.', - 'SETGID': 'Make arbitrary manipulations of process GIDs and supplementary GID list.', - 'SETUID': 'Make arbitrary manipulations of process UIDs.', - 'NET_BIND_SERVICE': 'Bind a socket to internet domain privileged ports (port numbers less than 1024).', - 'SYS_CHROOT': 'Use chroot(2), change root directory.', - 'SETFCAP': 'Set file capabilities.', - 'SYS_MODULE': 'Load and unload kernel modules.', - 'SYS_RAWIO': 'Perform I/O port operations (iopl(2) and ioperm(2)).', - 'SYS_PACCT': 'Use acct(2), switch process accounting on or off.', - 'SYS_ADMIN': 'Perform a range of system administration operations.', - 'SYS_NICE': 'Raise process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes.', - 'SYS_RESOURCE': 'Override resource Limits.', - 'SYS_TIME': 'Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock.', - 'SYS_TTY_CONFIG': 'Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals.', - 'AUDIT_CONTROL': 'Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules.', - 'MAC_ADMIN': 'Allow MAC configuration or state changes. Implemented for the Smack LSM.', - 'MAC_OVERRIDE': 'Override Mandatory Access Control (MAC). Implemented for the Smack Linux Security Module (LSM).', - 'NET_ADMIN': 'Perform various network-related operations.', - 'SYSLOG': 'Perform privileged syslog(2) operations.', - 'DAC_READ_SEARCH': 'Bypass file read permission checks and directory read and execute permission checks.', - 'LINUX_IMMUTABLE': 'Set the FS_APPEND_FL and FS_IMMUTABLE_FL i-node flags.', - 'NET_BROADCAST': 'Make socket broadcasts, and listen to multicasts.', - 'IPC_LOCK': 'Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2)).', - 'IPC_OWNER': 'Bypass permission checks for operations on System V IPC objects.', - 'SYS_PTRACE': 'Trace arbitrary processes using ptrace(2).', - 'SYS_BOOT': 'Use reboot(2) and kexec_load(2), reboot and load a new kernel for later execution.', - 'LEASE': 'Establish leases on arbitrary files (see fcntl(2)).', - 'WAKE_ALARM': 'Trigger something that will wake up the system.', - 'BLOCK_SUSPEND': 'Employ features that can block system suspend.' + SETPCAP: 'Modify process capabilities.', + MKNOD: 'Create special files using mknod(2).', + AUDIT_WRITE: 'Write records to kernel auditing log.', + CHOWN: 'Make arbitrary changes to file UIDs and GIDs (see chown(2)).', + NET_RAW: 'Use RAW and PACKET sockets.', + DAC_OVERRIDE: 'Bypass file read, write, and execute permission checks.', + FOWNER: 'Bypass permission checks on operations that normally require the file system UID of the process to match the UID of the file.', + FSETID: 'Don’t clear set-user-ID and set-group-ID permission bits when a file is modified.', + KILL: 'Bypass permission checks for sending signals.', + SETGID: 'Make arbitrary manipulations of process GIDs and supplementary GID list.', + SETUID: 'Make arbitrary manipulations of process UIDs.', + NET_BIND_SERVICE: 'Bind a socket to internet domain privileged ports (port numbers less than 1024).', + SYS_CHROOT: 'Use chroot(2), change root directory.', + SETFCAP: 'Set file capabilities.', + SYS_MODULE: 'Load and unload kernel modules.', + SYS_RAWIO: 'Perform I/O port operations (iopl(2) and ioperm(2)).', + SYS_PACCT: 'Use acct(2), switch process accounting on or off.', + SYS_ADMIN: 'Perform a range of system administration operations.', + SYS_NICE: 'Raise process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes.', + SYS_RESOURCE: 'Override resource Limits.', + SYS_TIME: 'Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock.', + SYS_TTY_CONFIG: 'Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals.', + AUDIT_CONTROL: 'Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules.', + MAC_ADMIN: 'Allow MAC configuration or state changes. Implemented for the Smack LSM.', + MAC_OVERRIDE: 'Override Mandatory Access Control (MAC). Implemented for the Smack Linux Security Module (LSM).', + NET_ADMIN: 'Perform various network-related operations.', + SYSLOG: 'Perform privileged syslog(2) operations.', + DAC_READ_SEARCH: 'Bypass file read permission checks and directory read and execute permission checks.', + LINUX_IMMUTABLE: 'Set the FS_APPEND_FL and FS_IMMUTABLE_FL i-node flags.', + NET_BROADCAST: 'Make socket broadcasts, and listen to multicasts.', + IPC_LOCK: 'Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2)).', + IPC_OWNER: 'Bypass permission checks for operations on System V IPC objects.', + SYS_PTRACE: 'Trace arbitrary processes using ptrace(2).', + SYS_BOOT: 'Use reboot(2) and kexec_load(2), reboot and load a new kernel for later execution.', + LEASE: 'Establish leases on arbitrary files (see fcntl(2)).', + WAKE_ALARM: 'Trigger something that will wake up the system.', + BLOCK_SUSPEND: 'Employ features that can block system suspend.', }; export function ContainerCapabilities() { - // all capabilities can be found at https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities - return [ - new ContainerCapability('SETPCAP', true), - new ContainerCapability('MKNOD', true), - new ContainerCapability('AUDIT_WRITE', true), - new ContainerCapability('CHOWN', true), - new ContainerCapability('NET_RAW', true), - new ContainerCapability('DAC_OVERRIDE', true), - new ContainerCapability('FOWNER', true), - new ContainerCapability('FSETID', true), - new ContainerCapability('KILL', true), - new ContainerCapability('SETGID', true), - new ContainerCapability('SETUID', true), - new ContainerCapability('NET_BIND_SERVICE', true), - new ContainerCapability('SYS_CHROOT', true), - new ContainerCapability('SETFCAP', true), - new ContainerCapability('SYS_MODULE', false), - new ContainerCapability('SYS_RAWIO', false), - new ContainerCapability('SYS_PACCT', false), - new ContainerCapability('SYS_ADMIN', false), - new ContainerCapability('SYS_NICE', false), - new ContainerCapability('SYS_RESOURCE', false), - new ContainerCapability('SYS_TIME', false), - new ContainerCapability('SYS_TTY_CONFIG', false), - new ContainerCapability('AUDIT_CONTROL', false), - new ContainerCapability('MAC_ADMIN', false), - new ContainerCapability('MAC_OVERRIDE', false), - new ContainerCapability('NET_ADMIN', false), - new ContainerCapability('SYSLOG', false), - new ContainerCapability('DAC_READ_SEARCH', false), - new ContainerCapability('LINUX_IMMUTABLE', false), - new ContainerCapability('NET_BROADCAST', false), - new ContainerCapability('IPC_LOCK', false), - new ContainerCapability('IPC_OWNER', false), - new ContainerCapability('SYS_PTRACE', false), - new ContainerCapability('SYS_BOOT', false), - new ContainerCapability('LEASE', false), - new ContainerCapability('WAKE_ALARM', false), - new ContainerCapability('BLOCK_SUSPEND', false) - ].sort(function (a, b) { - return a.capability < b.capability ? -1 : 1; - }); + // all capabilities can be found at https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities + return [ + new ContainerCapability('SETPCAP', true), + new ContainerCapability('MKNOD', true), + new ContainerCapability('AUDIT_WRITE', true), + new ContainerCapability('CHOWN', true), + new ContainerCapability('NET_RAW', true), + new ContainerCapability('DAC_OVERRIDE', true), + new ContainerCapability('FOWNER', true), + new ContainerCapability('FSETID', true), + new ContainerCapability('KILL', true), + new ContainerCapability('SETGID', true), + new ContainerCapability('SETUID', true), + new ContainerCapability('NET_BIND_SERVICE', true), + new ContainerCapability('SYS_CHROOT', true), + new ContainerCapability('SETFCAP', true), + new ContainerCapability('SYS_MODULE', false), + new ContainerCapability('SYS_RAWIO', false), + new ContainerCapability('SYS_PACCT', false), + new ContainerCapability('SYS_ADMIN', false), + new ContainerCapability('SYS_NICE', false), + new ContainerCapability('SYS_RESOURCE', false), + new ContainerCapability('SYS_TIME', false), + new ContainerCapability('SYS_TTY_CONFIG', false), + new ContainerCapability('AUDIT_CONTROL', false), + new ContainerCapability('MAC_ADMIN', false), + new ContainerCapability('MAC_OVERRIDE', false), + new ContainerCapability('NET_ADMIN', false), + new ContainerCapability('SYSLOG', false), + new ContainerCapability('DAC_READ_SEARCH', false), + new ContainerCapability('LINUX_IMMUTABLE', false), + new ContainerCapability('NET_BROADCAST', false), + new ContainerCapability('IPC_LOCK', false), + new ContainerCapability('IPC_OWNER', false), + new ContainerCapability('SYS_PTRACE', false), + new ContainerCapability('SYS_BOOT', false), + new ContainerCapability('LEASE', false), + new ContainerCapability('WAKE_ALARM', false), + new ContainerCapability('BLOCK_SUSPEND', false), + ].sort(function (a, b) { + return a.capability < b.capability ? -1 : 1; + }); } export function ContainerCapability(cap, allowed) { - this.capability = cap; - this.allowed = allowed; - this.description = capDesc[cap]; -} \ No newline at end of file + this.capability = cap; + this.allowed = allowed; + this.description = capDesc[cap]; +} diff --git a/app/docker/models/event.js b/app/docker/models/event.js index 46f9f8c10..914740a04 100644 --- a/app/docker/models/event.js +++ b/app/docker/models/event.js @@ -3,148 +3,148 @@ function createEventDetails(event) { var details = ''; switch (event.Type) { case 'container': - switch (event.Action) { - case 'stop': - details = 'Container ' + eventAttr.name + ' stopped'; - break; - case 'destroy': - details = 'Container ' + eventAttr.name + ' deleted'; - break; - case 'create': - details = 'Container ' + eventAttr.name + ' created'; - break; - case 'start': - details = 'Container ' + eventAttr.name + ' started'; - break; - case 'kill': - details = 'Container ' + eventAttr.name + ' killed'; - break; - case 'die': - details = 'Container ' + eventAttr.name + ' exited with status code ' + eventAttr.exitCode; - break; - case 'commit': - details = 'Container ' + eventAttr.name + ' committed'; - break; - case 'restart': - details = 'Container ' + eventAttr.name + ' restarted'; - break; - case 'pause': - details = 'Container ' + eventAttr.name + ' paused'; - break; - case 'unpause': - details = 'Container ' + eventAttr.name + ' unpaused'; - break; - case 'attach': - details = 'Container ' + eventAttr.name + ' attached'; - break; - case 'detach': - details = 'Container ' + eventAttr.name + ' detached'; - break; - case 'copy': - details = 'Container ' + eventAttr.name + ' copied'; - break; - case 'export': - details = 'Container ' + eventAttr.name + ' exported'; - break; - case 'health_status': - details = 'Container ' + eventAttr.name + ' executed health status'; - break; - case 'oom': - details = 'Container ' + eventAttr.name + ' goes in out of memory'; - break; - case 'rename': - details = 'Container ' + eventAttr.name + ' renamed'; - break; - case 'resize': - details = 'Container ' + eventAttr.name + ' resized'; - break; - case 'top': - details = 'Showed running processes for container ' + eventAttr.name; - break; - case 'update': - details = 'Container ' + eventAttr.name + ' updated'; - break; - default: - if (event.Action.indexOf('exec_create') === 0) { - details = 'Exec instance created'; - } else if (event.Action.indexOf('exec_start') === 0) { - details = 'Exec instance started'; - } else { - details = 'Unsupported event'; + switch (event.Action) { + case 'stop': + details = 'Container ' + eventAttr.name + ' stopped'; + break; + case 'destroy': + details = 'Container ' + eventAttr.name + ' deleted'; + break; + case 'create': + details = 'Container ' + eventAttr.name + ' created'; + break; + case 'start': + details = 'Container ' + eventAttr.name + ' started'; + break; + case 'kill': + details = 'Container ' + eventAttr.name + ' killed'; + break; + case 'die': + details = 'Container ' + eventAttr.name + ' exited with status code ' + eventAttr.exitCode; + break; + case 'commit': + details = 'Container ' + eventAttr.name + ' committed'; + break; + case 'restart': + details = 'Container ' + eventAttr.name + ' restarted'; + break; + case 'pause': + details = 'Container ' + eventAttr.name + ' paused'; + break; + case 'unpause': + details = 'Container ' + eventAttr.name + ' unpaused'; + break; + case 'attach': + details = 'Container ' + eventAttr.name + ' attached'; + break; + case 'detach': + details = 'Container ' + eventAttr.name + ' detached'; + break; + case 'copy': + details = 'Container ' + eventAttr.name + ' copied'; + break; + case 'export': + details = 'Container ' + eventAttr.name + ' exported'; + break; + case 'health_status': + details = 'Container ' + eventAttr.name + ' executed health status'; + break; + case 'oom': + details = 'Container ' + eventAttr.name + ' goes in out of memory'; + break; + case 'rename': + details = 'Container ' + eventAttr.name + ' renamed'; + break; + case 'resize': + details = 'Container ' + eventAttr.name + ' resized'; + break; + case 'top': + details = 'Showed running processes for container ' + eventAttr.name; + break; + case 'update': + details = 'Container ' + eventAttr.name + ' updated'; + break; + default: + if (event.Action.indexOf('exec_create') === 0) { + details = 'Exec instance created'; + } else if (event.Action.indexOf('exec_start') === 0) { + details = 'Exec instance started'; + } else { + details = 'Unsupported event'; + } } - } - break; + break; case 'image': - switch (event.Action) { - case 'delete': - details = 'Image deleted'; + switch (event.Action) { + case 'delete': + details = 'Image deleted'; + break; + case 'import': + details = 'Image ' + event.Actor.ID + ' imported'; + break; + case 'load': + details = 'Image ' + event.Actor.ID + ' loaded'; + break; + case 'tag': + details = 'New tag created for ' + eventAttr.name; + break; + case 'untag': + details = 'Image untagged'; + break; + case 'save': + details = 'Image ' + event.Actor.ID + ' saved'; + break; + case 'pull': + details = 'Image ' + event.Actor.ID + ' pulled'; + break; + case 'push': + details = 'Image ' + event.Actor.ID + ' pushed'; + break; + default: + details = 'Unsupported event'; + } break; - case 'import': - details = 'Image ' + event.Actor.ID + ' imported'; - break; - case 'load': - details = 'Image ' + event.Actor.ID + ' loaded'; - break; - case 'tag': - details = 'New tag created for ' + eventAttr.name; - break; - case 'untag': - details = 'Image untagged'; - break; - case 'save': - details = 'Image ' + event.Actor.ID + ' saved'; - break; - case 'pull': - details = 'Image ' + event.Actor.ID + ' pulled'; - break; - case 'push': - details = 'Image ' + event.Actor.ID + ' pushed'; - break; - default: - details = 'Unsupported event'; - } - break; case 'network': - switch (event.Action) { - case 'create': - details = 'Network ' + eventAttr.name + ' created'; + switch (event.Action) { + case 'create': + details = 'Network ' + eventAttr.name + ' created'; + break; + case 'destroy': + details = 'Network ' + eventAttr.name + ' deleted'; + break; + case 'remove': + details = 'Network ' + eventAttr.name + ' removed'; + break; + case 'connect': + details = 'Container connected to ' + eventAttr.name + ' network'; + break; + case 'disconnect': + details = 'Container disconnected from ' + eventAttr.name + ' network'; + break; + default: + details = 'Unsupported event'; + } break; - case 'destroy': - details = 'Network ' + eventAttr.name + ' deleted'; - break; - case 'remove': - details = 'Network ' + eventAttr.name + ' removed'; - break; - case 'connect': - details = 'Container connected to ' + eventAttr.name + ' network'; - break; - case 'disconnect': - details = 'Container disconnected from ' + eventAttr.name + ' network'; - break; - default: - details = 'Unsupported event'; - } - break; case 'volume': - switch (event.Action) { - case 'create': - details = 'Volume ' + event.Actor.ID + ' created'; + switch (event.Action) { + case 'create': + details = 'Volume ' + event.Actor.ID + ' created'; + break; + case 'destroy': + details = 'Volume ' + event.Actor.ID + ' deleted'; + break; + case 'mount': + details = 'Volume ' + event.Actor.ID + ' mounted'; + break; + case 'unmount': + details = 'Volume ' + event.Actor.ID + ' unmounted'; + break; + default: + details = 'Unsupported event'; + } break; - case 'destroy': - details = 'Volume ' + event.Actor.ID + ' deleted'; - break; - case 'mount': - details = 'Volume ' + event.Actor.ID + ' mounted'; - break; - case 'unmount': - details = 'Volume ' + event.Actor.ID + ' unmounted'; - break; - default: - details = 'Unsupported event'; - } - break; default: - details = 'Unsupported event'; + details = 'Unsupported event'; } return details; } diff --git a/app/docker/models/network.js b/app/docker/models/network.js index a629eb0d3..e3beb5e1f 100644 --- a/app/docker/models/network.js +++ b/app/docker/models/network.js @@ -29,4 +29,4 @@ export function NetworkViewModel(data) { this.ConfigFrom = data.ConfigFrom; this.ConfigOnly = data.ConfigOnly; -} \ No newline at end of file +} diff --git a/app/docker/models/node.js b/app/docker/models/node.js index 4057141f4..9386bf984 100644 --- a/app/docker/models/node.js +++ b/app/docker/models/node.js @@ -10,7 +10,7 @@ export function NodeViewModel(data) { var labels = data.Spec.Labels; if (labels) { - this.Labels = Object.keys(labels).map(function(key) { + this.Labels = Object.keys(labels).map(function (key) { return { key: key, value: labels[key], originalKey: key, originalValue: labels[key], added: true }; }); } else { @@ -19,7 +19,7 @@ export function NodeViewModel(data) { var engineLabels = data.Description.Engine.Labels; if (engineLabels) { - this.EngineLabels = Object.keys(engineLabels).map(function(key) { + this.EngineLabels = Object.keys(engineLabels).map(function (key) { return { key: key, value: engineLabels[key] }; }); } else { diff --git a/app/docker/models/porImageRegistry.js b/app/docker/models/porImageRegistry.js index 38ea4ad94..c34c318ab 100644 --- a/app/docker/models/porImageRegistry.js +++ b/app/docker/models/porImageRegistry.js @@ -1,11 +1,11 @@ /** * This model should be used with por-image-registry component * And bound to the 'model' attribute - * + * * // viewController.js - * + * * this.imageModel = new PorImageRegistryModel(); - * + * * // view.html * */ @@ -13,4 +13,4 @@ export function PorImageRegistryModel() { this.UseRegistry = true; this.Registry = {}; this.Image = ''; -} \ No newline at end of file +} diff --git a/app/docker/models/service.js b/app/docker/models/service.js index 84f58eed0..df72a282f 100644 --- a/app/docker/models/service.js +++ b/app/docker/models/service.js @@ -10,7 +10,7 @@ export function ServiceViewModel(data, runningTasks, allTasks) { this.Image = data.Spec.TaskTemplate.ContainerSpec.Image; this.Version = data.Version.Index; if (data.Spec.Mode.Replicated) { - this.Mode = 'replicated' ; + this.Mode = 'replicated'; this.Replicas = data.Spec.Mode.Replicated.Replicas; } else { this.Mode = 'global'; @@ -23,12 +23,12 @@ export function ServiceViewModel(data, runningTasks, allTasks) { } if (data.Spec.TaskTemplate.Resources) { if (data.Spec.TaskTemplate.Resources.Limits) { - this.LimitNanoCPUs = data.Spec.TaskTemplate.Resources.Limits.NanoCPUs; - this.LimitMemoryBytes = data.Spec.TaskTemplate.Resources.Limits.MemoryBytes; + this.LimitNanoCPUs = data.Spec.TaskTemplate.Resources.Limits.NanoCPUs; + this.LimitMemoryBytes = data.Spec.TaskTemplate.Resources.Limits.MemoryBytes; } if (data.Spec.TaskTemplate.Resources.Reservations) { - this.ReservationNanoCPUs = data.Spec.TaskTemplate.Resources.Reservations.NanoCPUs; - this.ReservationMemoryBytes = data.Spec.TaskTemplate.Resources.Reservations.MemoryBytes; + this.ReservationNanoCPUs = data.Spec.TaskTemplate.Resources.Reservations.NanoCPUs; + this.ReservationMemoryBytes = data.Spec.TaskTemplate.Resources.Reservations.MemoryBytes; } } @@ -92,7 +92,7 @@ export function ServiceViewModel(data, runningTasks, allTasks) { this.VirtualIPs = data.Endpoint ? data.Endpoint.VirtualIPs : []; if (data.Spec.UpdateConfig) { - this.UpdateParallelism = (typeof data.Spec.UpdateConfig.Parallelism !== undefined) ? data.Spec.UpdateConfig.Parallelism || 0 : 1; + this.UpdateParallelism = typeof data.Spec.UpdateConfig.Parallelism !== undefined ? data.Spec.UpdateConfig.Parallelism || 0 : 1; this.UpdateDelay = data.Spec.UpdateConfig.Delay || 0; this.UpdateFailureAction = data.Spec.UpdateConfig.FailureAction || 'pause'; this.UpdateOrder = data.Spec.UpdateConfig.Order || 'stop-first'; diff --git a/app/docker/rest/build.js b/app/docker/rest/build.js index e48d12d51..b0ba607ad 100644 --- a/app/docker/rest/build.js +++ b/app/docker/rest/build.js @@ -1,20 +1,31 @@ import { jsonObjectsToArrayHandler } from './response/handlers'; -angular.module('portainer.docker') -.factory('Build', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BuildFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/build', { - endpointId: EndpointProvider.endpointID +angular.module('portainer.docker').factory('Build', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function BuildFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/build', + { + endpointId: EndpointProvider.endpointID, + }, + { + buildImage: { + method: 'POST', + ignoreLoadingBar: true, + transformResponse: jsonObjectsToArrayHandler, + isArray: true, + headers: { 'Content-Type': 'application/x-tar' }, + }, + buildImageOverride: { + method: 'POST', + ignoreLoadingBar: true, + transformResponse: jsonObjectsToArrayHandler, + isArray: true, + }, + } + ); }, - { - buildImage: { - method: 'POST', ignoreLoadingBar: true, - transformResponse: jsonObjectsToArrayHandler, isArray: true, - headers: { 'Content-Type': 'application/x-tar' } - }, - buildImageOverride: { - method: 'POST', ignoreLoadingBar: true, - transformResponse: jsonObjectsToArrayHandler, isArray: true - } - }); -}]); +]); diff --git a/app/docker/rest/commit.js b/app/docker/rest/commit.js index 364610297..5d768a119 100644 --- a/app/docker/rest/commit.js +++ b/app/docker/rest/commit.js @@ -1,10 +1,17 @@ -angular.module('portainer.docker') -.factory('Commit', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function CommitFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/commit', { - endpointId: EndpointProvider.endpointID +angular.module('portainer.docker').factory('Commit', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function CommitFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/commit', + { + endpointId: EndpointProvider.endpointID, + }, + { + commitContainer: { method: 'POST', params: { container: '@id', repo: '@repo' }, ignoreLoadingBar: true }, + } + ); }, - { - commitContainer: {method: 'POST', params: {container: '@id', repo: '@repo'}, ignoreLoadingBar: true} - }); -}]); +]); diff --git a/app/docker/rest/config.js b/app/docker/rest/config.js index 2b21ee2d0..af3bcecc7 100644 --- a/app/docker/rest/config.js +++ b/app/docker/rest/config.js @@ -1,12 +1,20 @@ -angular.module('portainer.docker') -.factory('Config', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ConfigFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/configs/:id/:action', { - endpointId: EndpointProvider.endpointID - }, { - get: { method: 'GET', params: { id: '@id' } }, - query: { method: 'GET', isArray: true }, - create: { method: 'POST', params: { action: 'create' }, ignoreLoadingBar: true }, - remove: { method: 'DELETE', params: { id: '@id' } } - }); -}]); +angular.module('portainer.docker').factory('Config', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function ConfigFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/configs/:id/:action', + { + endpointId: EndpointProvider.endpointID, + }, + { + get: { method: 'GET', params: { id: '@id' } }, + query: { method: 'GET', isArray: true }, + create: { method: 'POST', params: { action: 'create' }, ignoreLoadingBar: true }, + remove: { method: 'DELETE', params: { id: '@id' } }, + } + ); + }, +]); diff --git a/app/docker/rest/container.js b/app/docker/rest/container.js index 1aeb38e26..aae286f11 100644 --- a/app/docker/rest/container.js +++ b/app/docker/rest/container.js @@ -1,82 +1,115 @@ -import {genericHandler, logsHandler} from './response/handlers'; +import { genericHandler, logsHandler } from './response/handlers'; -angular.module('portainer.docker') -.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'ContainersInterceptor', -function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, ContainersInterceptor) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/containers/:id/:action', { - name: '@name', - endpointId: EndpointProvider.endpointID +angular.module('portainer.docker').factory('Container', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + 'ContainersInterceptor', + function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, ContainersInterceptor) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/containers/:id/:action', + { + name: '@name', + endpointId: EndpointProvider.endpointID, + }, + { + query: { + method: 'GET', + params: { all: 0, action: 'json', filters: '@filters' }, + isArray: true, + interceptor: ContainersInterceptor, + timeout: 15000, + }, + get: { + method: 'GET', + params: { action: 'json' }, + }, + stop: { + method: 'POST', + params: { id: '@id', action: 'stop' }, + }, + restart: { + method: 'POST', + params: { id: '@id', action: 'restart' }, + }, + kill: { + method: 'POST', + params: { id: '@id', action: 'kill' }, + }, + pause: { + method: 'POST', + params: { id: '@id', action: 'pause' }, + }, + unpause: { + method: 'POST', + params: { id: '@id', action: 'unpause' }, + }, + logs: { + method: 'GET', + params: { id: '@id', action: 'logs' }, + timeout: 4500, + ignoreLoadingBar: true, + transformResponse: logsHandler, + }, + stats: { + method: 'GET', + params: { id: '@id', stream: false, action: 'stats' }, + timeout: 4500, + ignoreLoadingBar: true, + }, + top: { + method: 'GET', + params: { id: '@id', action: 'top' }, + timeout: 4500, + ignoreLoadingBar: true, + }, + start: { + method: 'POST', + params: { id: '@id', action: 'start' }, + transformResponse: genericHandler, + }, + create: { + method: 'POST', + params: { action: 'create' }, + transformResponse: genericHandler, + ignoreLoadingBar: true, + }, + remove: { + method: 'DELETE', + params: { id: '@id', v: '@v', force: '@force' }, + transformResponse: genericHandler, + }, + rename: { + method: 'POST', + params: { id: '@id', action: 'rename', name: '@name' }, + transformResponse: genericHandler, + }, + exec: { + method: 'POST', + params: { id: '@id', action: 'exec' }, + transformResponse: genericHandler, + ignoreLoadingBar: true, + }, + inspect: { + method: 'GET', + params: { id: '@id', action: 'json' }, + }, + update: { + method: 'POST', + params: { id: '@id', action: 'update' }, + }, + prune: { + method: 'POST', + params: { action: 'prune', filters: '@filters' }, + }, + resize: { + method: 'POST', + params: { id: '@id', action: 'resize', h: '@height', w: '@width' }, + transformResponse: genericHandler, + ignoreLoadingBar: true, + }, + } + ); }, - { - query: { - method: 'GET', params: { all: 0, action: 'json', filters: '@filters' }, - isArray: true, interceptor: ContainersInterceptor, timeout: 15000 - }, - get: { - method: 'GET', params: { action: 'json' } - }, - stop: { - method: 'POST', params: { id: '@id', action: 'stop' } - }, - restart: { - method: 'POST', params: { id: '@id', action: 'restart' } - }, - kill: { - method: 'POST', params: { id: '@id', action: 'kill' } - }, - pause: { - method: 'POST', params: { id: '@id', action: 'pause' } - }, - unpause: { - method: 'POST', params: { id: '@id', action: 'unpause' } - }, - logs: { - method: 'GET', params: { id: '@id', action: 'logs' }, - timeout: 4500, ignoreLoadingBar: true, - transformResponse: logsHandler - }, - stats: { - method: 'GET', params: { id: '@id', stream: false, action: 'stats' }, - timeout: 4500, ignoreLoadingBar: true - }, - top: { - method: 'GET', params: { id: '@id', action: 'top' }, - timeout: 4500, ignoreLoadingBar: true - }, - start: { - method: 'POST', params: {id: '@id', action: 'start'}, - transformResponse: genericHandler - }, - create: { - method: 'POST', params: {action: 'create'}, - transformResponse: genericHandler, - ignoreLoadingBar: true - }, - remove: { - method: 'DELETE', params: {id: '@id', v: '@v', force: '@force'}, - transformResponse: genericHandler - }, - rename: { - method: 'POST', params: { id: '@id', action: 'rename', name: '@name' }, - transformResponse: genericHandler - }, - exec: { - method: 'POST', params: {id: '@id', action: 'exec'}, - transformResponse: genericHandler, ignoreLoadingBar: true - }, - inspect: { - method: 'GET', params: { id: '@id', action: 'json' } - }, - update: { - method: 'POST', params: { id: '@id', action: 'update'} - }, - prune: { - method: 'POST', params: { action: 'prune', filters: '@filters' } - }, - resize: { - method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'}, - transformResponse: genericHandler, ignoreLoadingBar: true - } - }); -}]); +]); diff --git a/app/docker/rest/exec.js b/app/docker/rest/exec.js index 6ad7b4f35..20c5035e4 100644 --- a/app/docker/rest/exec.js +++ b/app/docker/rest/exec.js @@ -1,16 +1,24 @@ import { genericHandler } from './response/handlers'; -angular.module('portainer.docker') -.factory('Exec', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', -function ExecFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/exec/:id/:action', { - endpointId: EndpointProvider.endpointID +angular.module('portainer.docker').factory('Exec', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function ExecFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/exec/:id/:action', + { + endpointId: EndpointProvider.endpointID, + }, + { + resize: { + method: 'POST', + params: { id: '@id', action: 'resize', h: '@height', w: '@width' }, + transformResponse: genericHandler, + ignoreLoadingBar: true, + }, + } + ); }, - { - resize: { - method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'}, - transformResponse: genericHandler, ignoreLoadingBar: true - } - }); -}]); +]); diff --git a/app/docker/rest/image.js b/app/docker/rest/image.js index a0cfd0cfc..aa0d8a6a9 100644 --- a/app/docker/rest/image.js +++ b/app/docker/rest/image.js @@ -1,43 +1,58 @@ -import {deleteImageHandler, jsonObjectsToArrayHandler} from './response/handlers'; -import {imageGetResponse} from './response/image'; +import { deleteImageHandler, jsonObjectsToArrayHandler } from './response/handlers'; +import { imageGetResponse } from './response/image'; -angular.module('portainer.docker') -.factory('Image', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper', 'ImagesInterceptor', -function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper, ImagesInterceptor) { - 'use strict'; +angular.module('portainer.docker').factory('Image', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + 'HttpRequestHelper', + 'ImagesInterceptor', + function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper, ImagesInterceptor) { + 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/images/:id/:action', { - endpointId: EndpointProvider.endpointID + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/images/:id/:action', + { + endpointId: EndpointProvider.endpointID, + }, + { + query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true, interceptor: ImagesInterceptor, timeout: 15000 }, + get: { method: 'GET', params: { action: 'json' } }, + search: { method: 'GET', params: { action: 'search' } }, + history: { method: 'GET', params: { action: 'history' }, isArray: true }, + insert: { method: 'POST', params: { id: '@id', action: 'insert' } }, + tag: { method: 'POST', params: { id: '@id', action: 'tag', force: 0, repo: '@repo' }, ignoreLoadingBar: true }, + inspect: { method: 'GET', params: { id: '@id', action: 'json' } }, + push: { + method: 'POST', + params: { action: 'push', id: '@imageName' }, + isArray: true, + transformResponse: jsonObjectsToArrayHandler, + headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader }, + ignoreLoadingBar: true, + }, + create: { + method: 'POST', + params: { action: 'create', fromImage: '@fromImage' }, + isArray: true, + transformResponse: jsonObjectsToArrayHandler, + headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader }, + ignoreLoadingBar: true, + }, + download: { + method: 'GET', + params: { action: 'get', names: '@names' }, + transformResponse: imageGetResponse, + responseType: 'blob', + ignoreLoadingBar: true, + }, + remove: { + method: 'DELETE', + params: { id: '@id', force: '@force' }, + isArray: true, + transformResponse: deleteImageHandler, + }, + } + ); }, - { - query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true, interceptor: ImagesInterceptor, timeout: 15000}, - get: {method: 'GET', params: {action: 'json'}}, - search: {method: 'GET', params: {action: 'search'}}, - history: {method: 'GET', params: {action: 'history'}, isArray: true}, - insert: {method: 'POST', params: {id: '@id', action: 'insert'}}, - tag: {method: 'POST', params: {id: '@id', action: 'tag', force: 0, repo: '@repo'}, ignoreLoadingBar: true}, - inspect: {method: 'GET', params: {id: '@id', action: 'json'}}, - push: { - method: 'POST', params: {action: 'push', id: '@imageName'}, - isArray: true, transformResponse: jsonObjectsToArrayHandler, - headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader }, - ignoreLoadingBar: true - }, - create: { - method: 'POST', params: {action: 'create', fromImage: '@fromImage'}, - isArray: true, transformResponse: jsonObjectsToArrayHandler, - headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader }, - ignoreLoadingBar: true - }, - download: { - method: 'GET', params: {action:'get', names: '@names'}, - transformResponse: imageGetResponse, - responseType: 'blob', - ignoreLoadingBar: true - }, - remove: { - method: 'DELETE', params: {id: '@id', force: '@force'}, - isArray: true, transformResponse: deleteImageHandler - } - }); -}]); +]); diff --git a/app/docker/rest/network.js b/app/docker/rest/network.js index 5830483ef..8c8480158 100644 --- a/app/docker/rest/network.js +++ b/app/docker/rest/network.js @@ -1,32 +1,47 @@ -import {genericHandler} from './response/handlers'; +import { genericHandler } from './response/handlers'; -angular.module('portainer.docker') -.factory('Network', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'NetworksInterceptor', -function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, NetworksInterceptor) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/networks/:id/:action', { - id: '@id', - endpointId: EndpointProvider.endpointID +angular.module('portainer.docker').factory('Network', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + 'NetworksInterceptor', + function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, NetworksInterceptor) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/networks/:id/:action', + { + id: '@id', + endpointId: EndpointProvider.endpointID, + }, + { + query: { + method: 'GET', + isArray: true, + interceptor: NetworksInterceptor, + timeout: 15000, + }, + get: { + method: 'GET', + }, + create: { + method: 'POST', + params: { action: 'create' }, + transformResponse: genericHandler, + ignoreLoadingBar: true, + }, + remove: { + method: 'DELETE', + transformResponse: genericHandler, + }, + connect: { + method: 'POST', + params: { action: 'connect' }, + }, + disconnect: { + method: 'POST', + params: { action: 'disconnect' }, + }, + } + ); }, - { - query: { - method: 'GET', isArray: true, interceptor: NetworksInterceptor, timeout: 15000 - }, - get: { - method: 'GET' - }, - create: { - method: 'POST', params: {action: 'create'}, - transformResponse: genericHandler, ignoreLoadingBar: true - }, - remove: { - method: 'DELETE', transformResponse: genericHandler - }, - connect: { - method: 'POST', params: { action: 'connect' } - }, - disconnect: { - method: 'POST', params: { action: 'disconnect' } - } - }); -}]); +]); diff --git a/app/docker/rest/node.js b/app/docker/rest/node.js index 821e40328..aa35d1a13 100644 --- a/app/docker/rest/node.js +++ b/app/docker/rest/node.js @@ -1,13 +1,20 @@ -angular.module('portainer.docker') -.factory('Node', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function NodeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/nodes/:id/:action', { - endpointId: EndpointProvider.endpointID +angular.module('portainer.docker').factory('Node', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function NodeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/nodes/:id/:action', + { + endpointId: EndpointProvider.endpointID, + }, + { + query: { method: 'GET', isArray: true }, + get: { method: 'GET', params: { id: '@id' } }, + update: { method: 'POST', params: { id: '@id', action: 'update', version: '@version' } }, + remove: { method: 'DELETE', params: { id: '@id' } }, + } + ); }, - { - query: {method: 'GET', isArray: true}, - get: {method: 'GET', params: {id: '@id'}}, - update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} }, - remove: { method: 'DELETE', params: {id: '@id'} } - }); -}]); +]); diff --git a/app/docker/rest/plugin.js b/app/docker/rest/plugin.js index c73817fd4..ed2e5784b 100644 --- a/app/docker/rest/plugin.js +++ b/app/docker/rest/plugin.js @@ -1,9 +1,17 @@ -angular.module('portainer.docker') -.factory('Plugin', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function PluginFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/plugins/:id/:action', { - endpointId: EndpointProvider.endpointID - }, { - query: { method: 'GET', isArray: true } - }); -}]); +angular.module('portainer.docker').factory('Plugin', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function PluginFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/plugins/:id/:action', + { + endpointId: EndpointProvider.endpointID, + }, + { + query: { method: 'GET', isArray: true }, + } + ); + }, +]); diff --git a/app/docker/rest/response/handlers.js b/app/docker/rest/response/handlers.js index 1a3854c34..7e14da023 100644 --- a/app/docker/rest/response/handlers.js +++ b/app/docker/rest/response/handlers.js @@ -1,15 +1,14 @@ function isJSONArray(jsonString) { - return Object.prototype.toString.call(jsonString) === '[object Array]'; + return Object.prototype.toString.call(jsonString) === '[object Array]'; } function isJSON(jsonString) { try { var o = JSON.parse(jsonString); if (o && typeof o === 'object') { - return o; + return o; } - } - catch (e) { + } catch (e) { //empty } return false; @@ -50,7 +49,7 @@ export function genericHandler(data) { // This handler wraps the data in a JSON object under the "logs" property. export function logsHandler(data) { return { - logs: data + logs: data, }; } @@ -63,7 +62,7 @@ export function deleteImageHandler(data) { // A string is returned on failure (Docker < 1.12) var response = []; if (!isJSON(data)) { - response.push({message: data}); + response.push({ message: data }); } // A JSON object is returned on failure (Docker = 1.12) else if (!isJSONArray(data)) { diff --git a/app/docker/rest/response/image.js b/app/docker/rest/response/image.js index c2e63cd82..7d61e5943 100644 --- a/app/docker/rest/response/image.js +++ b/app/docker/rest/response/image.js @@ -3,8 +3,7 @@ // This functions simply creates a response object and assign // the data to a field. export function imageGetResponse(data) { - var response = {}; - response.file = data; - return response; - } - \ No newline at end of file + var response = {}; + response.file = data; + return response; +} diff --git a/app/docker/rest/secret.js b/app/docker/rest/secret.js index 1f5f8ab95..9969a18d2 100644 --- a/app/docker/rest/secret.js +++ b/app/docker/rest/secret.js @@ -1,12 +1,20 @@ -angular.module('portainer.docker') -.factory('Secret', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function SecretFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/secrets/:id/:action', { - endpointId: EndpointProvider.endpointID - }, { - get: { method: 'GET', params: {id: '@id'} }, - query: { method: 'GET', isArray: true }, - create: { method: 'POST', params: {action: 'create'}, ignoreLoadingBar: true }, - remove: { method: 'DELETE', params: {id: '@id'} } - }); -}]); +angular.module('portainer.docker').factory('Secret', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function SecretFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/secrets/:id/:action', + { + endpointId: EndpointProvider.endpointID, + }, + { + get: { method: 'GET', params: { id: '@id' } }, + query: { method: 'GET', isArray: true }, + create: { method: 'POST', params: { action: 'create' }, ignoreLoadingBar: true }, + remove: { method: 'DELETE', params: { id: '@id' } }, + } + ); + }, +]); diff --git a/app/docker/rest/service.js b/app/docker/rest/service.js index 81a8eca7e..aecd4acbe 100644 --- a/app/docker/rest/service.js +++ b/app/docker/rest/service.js @@ -1,34 +1,45 @@ import { logsHandler } from './response/handlers'; -angular.module('portainer.docker') -.factory('Service', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper', -function ServiceFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/services/:id/:action', { - endpointId: EndpointProvider.endpointID - }, - { - get: { method: 'GET', params: {id: '@id'} }, - query: { method: 'GET', isArray: true, params: {filters: '@filters'} }, - create: { - method: 'POST', params: {action: 'create'}, - headers: { - 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader, - 'version': '1.29' +angular.module('portainer.docker').factory('Service', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + 'HttpRequestHelper', + function ServiceFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/services/:id/:action', + { + endpointId: EndpointProvider.endpointID, }, - ignoreLoadingBar: true - }, - update: { - method: 'POST', params: { id: '@id', action: 'update', version: '@version', rollback: '@rollback' }, - headers: { - 'version': '1.29' + { + get: { method: 'GET', params: { id: '@id' } }, + query: { method: 'GET', isArray: true, params: { filters: '@filters' } }, + create: { + method: 'POST', + params: { action: 'create' }, + headers: { + 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader, + version: '1.29', + }, + ignoreLoadingBar: true, + }, + update: { + method: 'POST', + params: { id: '@id', action: 'update', version: '@version', rollback: '@rollback' }, + headers: { + version: '1.29', + }, + }, + remove: { method: 'DELETE', params: { id: '@id' } }, + logs: { + method: 'GET', + params: { id: '@id', action: 'logs' }, + timeout: 4500, + ignoreLoadingBar: true, + transformResponse: logsHandler, + }, } - }, - remove: { method: 'DELETE', params: {id: '@id'} }, - logs: { - method: 'GET', params: { id: '@id', action: 'logs' }, - timeout: 4500, ignoreLoadingBar: true, - transformResponse: logsHandler - } - }); -}]); + ); + }, +]); diff --git a/app/docker/rest/swarm.js b/app/docker/rest/swarm.js index de4cc85a4..0cdc1fd9a 100644 --- a/app/docker/rest/swarm.js +++ b/app/docker/rest/swarm.js @@ -1,10 +1,17 @@ -angular.module('portainer.docker') -.factory('Swarm', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function SwarmFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/swarm', { - endpointId: EndpointProvider.endpointID +angular.module('portainer.docker').factory('Swarm', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function SwarmFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/swarm', + { + endpointId: EndpointProvider.endpointID, + }, + { + get: { method: 'GET' }, + } + ); }, - { - get: { method: 'GET' } - }); -}]); +]); diff --git a/app/docker/rest/system.js b/app/docker/rest/system.js index 844ae046f..fa69df952 100644 --- a/app/docker/rest/system.js +++ b/app/docker/rest/system.js @@ -1,23 +1,36 @@ -import {jsonObjectsToArrayHandler} from './response/handlers'; +import { jsonObjectsToArrayHandler } from './response/handlers'; -angular.module('portainer.docker') -.factory('System', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'InfoInterceptor', 'VersionInterceptor', +angular.module('portainer.docker').factory('System', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + 'InfoInterceptor', + 'VersionInterceptor', function SystemFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, InfoInterceptor, VersionInterceptor) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/:action/:subAction', { - name: '@name', - endpointId: EndpointProvider.endpointID + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/:action/:subAction', + { + name: '@name', + endpointId: EndpointProvider.endpointID, + }, + { + info: { + method: 'GET', + params: { action: 'info' }, + timeout: 15000, + interceptor: InfoInterceptor, + }, + version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor }, + events: { + method: 'GET', + params: { action: 'events', since: '@since', until: '@until' }, + isArray: true, + transformResponse: jsonObjectsToArrayHandler, + }, + auth: { method: 'POST', params: { action: 'auth' } }, + dataUsage: { method: 'GET', params: { action: 'system', subAction: 'df' } }, + } + ); }, - { - info: { - method: 'GET', params: { action: 'info' }, timeout: 15000, interceptor: InfoInterceptor - }, - version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor }, - events: { - method: 'GET', params: { action: 'events', since: '@since', until: '@until' }, - isArray: true, transformResponse: jsonObjectsToArrayHandler - }, - auth: { method: 'POST', params: { action: 'auth' } }, - dataUsage: { method: 'GET', params: { action: 'system', subAction: 'df' } } - }); -}]); +]); diff --git a/app/docker/rest/systemEndpoint.js b/app/docker/rest/systemEndpoint.js index c54b8bda1..2bdad3df3 100644 --- a/app/docker/rest/systemEndpoint.js +++ b/app/docker/rest/systemEndpoint.js @@ -1,13 +1,19 @@ -angular.module('portainer.docker') -.factory('SystemEndpoint', ['$resource', 'API_ENDPOINT_ENDPOINTS', +angular.module('portainer.docker').factory('SystemEndpoint', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', function SystemEndpointFactory($resource, API_ENDPOINT_ENDPOINTS) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/:action/:subAction', { - name: '@name' + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/:action/:subAction', + { + name: '@name', + }, + { + ping: { + method: 'GET', + params: { action: '_ping', endpointId: '@endpointId' }, + }, + } + ); }, - { - ping: { - method: 'GET', params: { action: '_ping', endpointId: '@endpointId' } - } - }); -}]); +]); diff --git a/app/docker/rest/task.js b/app/docker/rest/task.js index b4860784f..e2fa19603 100644 --- a/app/docker/rest/task.js +++ b/app/docker/rest/task.js @@ -1,18 +1,27 @@ import { logsHandler } from './response/handlers'; -angular.module('portainer.docker') -.factory('Task', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function TaskFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id/:action', { - endpointId: EndpointProvider.endpointID +angular.module('portainer.docker').factory('Task', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function TaskFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id/:action', + { + endpointId: EndpointProvider.endpointID, + }, + { + get: { method: 'GET', params: { id: '@id' } }, + query: { method: 'GET', isArray: true, params: { filters: '@filters' } }, + logs: { + method: 'GET', + params: { id: '@id', action: 'logs' }, + timeout: 4500, + ignoreLoadingBar: true, + transformResponse: logsHandler, + }, + } + ); }, - { - get: { method: 'GET', params: {id: '@id'} }, - query: { method: 'GET', isArray: true, params: {filters: '@filters'} }, - logs: { - method: 'GET', params: { id: '@id', action: 'logs' }, - timeout: 4500, ignoreLoadingBar: true, - transformResponse: logsHandler - } - }); -}]); +]); diff --git a/app/docker/rest/volume.js b/app/docker/rest/volume.js index 1a85ceef6..2a6f44165 100644 --- a/app/docker/rest/volume.js +++ b/app/docker/rest/volume.js @@ -1,19 +1,27 @@ -import {genericHandler} from './response/handlers'; +import { genericHandler } from './response/handlers'; -angular.module('portainer.docker') -.factory('Volume', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'VolumesInterceptor', +angular.module('portainer.docker').factory('Volume', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + 'VolumesInterceptor', function VolumeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, VolumesInterceptor) { - 'use strict'; - return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/volumes/:id/:action', - { - endpointId: EndpointProvider.endpointID + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/volumes/:id/:action', + { + endpointId: EndpointProvider.endpointID, + }, + { + query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000 }, + get: { method: 'GET', params: { id: '@id' } }, + create: { method: 'POST', params: { action: 'create' }, transformResponse: genericHandler, ignoreLoadingBar: true }, + remove: { + method: 'DELETE', + transformResponse: genericHandler, + params: { id: '@id' }, + }, + } + ); }, - { - query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000}, - get: { method: 'GET', params: {id: '@id'} }, - create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler, ignoreLoadingBar: true}, - remove: { - method: 'DELETE', transformResponse: genericHandler, params: {id: '@id'} - } - }); -}]); +]); diff --git a/app/docker/services/buildService.js b/app/docker/services/buildService.js index 53f50688e..e9e5fc433 100644 --- a/app/docker/services/buildService.js +++ b/app/docker/services/buildService.js @@ -1,67 +1,71 @@ -import { ImageBuildModel } from "../models/image"; +import { ImageBuildModel } from '../models/image'; -angular.module('portainer.docker') -.factory('BuildService', ['$q', 'Build', 'FileUploadService', function BuildServiceFactory($q, Build, FileUploadService) { - 'use strict'; - var service = {}; +angular.module('portainer.docker').factory('BuildService', [ + '$q', + 'Build', + 'FileUploadService', + function BuildServiceFactory($q, Build, FileUploadService) { + 'use strict'; + var service = {}; - service.buildImageFromUpload = function(names, file, path) { - var deferred = $q.defer(); + service.buildImageFromUpload = function (names, file, path) { + var deferred = $q.defer(); - FileUploadService.buildImage(names, file, path) - .then(function success(response) { - var model = new ImageBuildModel(response.data); - deferred.resolve(model); - }) - .catch(function error(err) { - deferred.reject(err); - }); + FileUploadService.buildImage(names, file, path) + .then(function success(response) { + var model = new ImageBuildModel(response.data); + deferred.resolve(model); + }) + .catch(function error(err) { + deferred.reject(err); + }); - return deferred.promise; - }; - - service.buildImageFromURL = function(names, url, path) { - var params = { - t: names, - remote: url, - dockerfile: path + return deferred.promise; }; - var deferred = $q.defer(); + service.buildImageFromURL = function (names, url, path) { + var params = { + t: names, + remote: url, + dockerfile: path, + }; - Build.buildImage(params, {}).$promise - .then(function success(data) { - var model = new ImageBuildModel(data); - deferred.resolve(model); - }) - .catch(function error(err) { - deferred.reject(err); - }); + var deferred = $q.defer(); - return deferred.promise; - }; + Build.buildImage(params, {}) + .$promise.then(function success(data) { + var model = new ImageBuildModel(data); + deferred.resolve(model); + }) + .catch(function error(err) { + deferred.reject(err); + }); - service.buildImageFromDockerfileContent = function(names, content) { - var params = { - t: names - }; - var payload = { - content: content + return deferred.promise; }; - var deferred = $q.defer(); + service.buildImageFromDockerfileContent = function (names, content) { + var params = { + t: names, + }; + var payload = { + content: content, + }; - Build.buildImageOverride(params, payload).$promise - .then(function success(data) { - var model = new ImageBuildModel(data); - deferred.resolve(model); - }) - .catch(function error(err) { - deferred.reject(err); - }); + var deferred = $q.defer(); - return deferred.promise; - }; + Build.buildImageOverride(params, payload) + .$promise.then(function success(data) { + var model = new ImageBuildModel(data); + deferred.resolve(model); + }) + .catch(function error(err) { + deferred.reject(err); + }); - return service; -}]); + return deferred.promise; + }; + + return service; + }, +]); diff --git a/app/docker/services/configService.js b/app/docker/services/configService.js index 520e190a6..1d773eeaf 100644 --- a/app/docker/services/configService.js +++ b/app/docker/services/configService.js @@ -1,63 +1,66 @@ import { ConfigViewModel } from '../models/config'; -angular.module('portainer.docker') -.factory('ConfigService', ['$q', 'Config', function ConfigServiceFactory($q, Config) { - 'use strict'; - var service = {}; +angular.module('portainer.docker').factory('ConfigService', [ + '$q', + 'Config', + function ConfigServiceFactory($q, Config) { + 'use strict'; + var service = {}; - service.config = function(configId) { - var deferred = $q.defer(); + service.config = function (configId) { + var deferred = $q.defer(); - Config.get({id: configId}).$promise - .then(function success(data) { - var config = new ConfigViewModel(data); - deferred.resolve(config); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve config details', err: err }); - }); + Config.get({ id: configId }) + .$promise.then(function success(data) { + var config = new ConfigViewModel(data); + deferred.resolve(config); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve config details', err: err }); + }); - return deferred.promise; - }; + return deferred.promise; + }; - service.configs = function() { - var deferred = $q.defer(); + service.configs = function () { + var deferred = $q.defer(); - Config.query({}).$promise - .then(function success(data) { - var configs = data.map(function (item) { - return new ConfigViewModel(item); - }); - deferred.resolve(configs); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve configs', err: err }); - }); + Config.query({}) + .$promise.then(function success(data) { + var configs = data.map(function (item) { + return new ConfigViewModel(item); + }); + deferred.resolve(configs); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve configs', err: err }); + }); - return deferred.promise; - }; + return deferred.promise; + }; - service.remove = function(configId) { - var deferred = $q.defer(); + service.remove = function (configId) { + var deferred = $q.defer(); - Config.remove({ id: configId }).$promise - .then(function success(data) { - if (data.message) { - deferred.reject({ msg: data.message }); - } else { - deferred.resolve(); - } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to remove config', err: err }); - }); + Config.remove({ id: configId }) + .$promise.then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message }); + } else { + deferred.resolve(); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to remove config', err: err }); + }); - return deferred.promise; - }; + return deferred.promise; + }; - service.create = function(config) { - return Config.create(config).$promise; - }; + service.create = function (config) { + return Config.create(config).$promise; + }; - return service; -}]); + return service; + }, +]); diff --git a/app/docker/services/containerService.js b/app/docker/services/containerService.js index 300a09f91..65a90ba72 100644 --- a/app/docker/services/containerService.js +++ b/app/docker/services/containerService.js @@ -1,213 +1,217 @@ import { ContainerDetailsViewModel, ContainerViewModel, ContainerStatsViewModel } from '../models/container'; -angular.module('portainer.docker') -.factory('ContainerService', ['$q', 'Container', 'ResourceControlService', 'LogHelper', '$timeout', -function ContainerServiceFactory($q, Container, ResourceControlService, LogHelper, $timeout) { - 'use strict'; - var service = {}; +angular.module('portainer.docker').factory('ContainerService', [ + '$q', + 'Container', + 'ResourceControlService', + 'LogHelper', + '$timeout', + function ContainerServiceFactory($q, Container, ResourceControlService, LogHelper, $timeout) { + 'use strict'; + var service = {}; - service.container = function(id) { - var deferred = $q.defer(); + service.container = function (id) { + var deferred = $q.defer(); - Container.get({ id: id }).$promise - .then(function success(data) { - var container = new ContainerDetailsViewModel(data); - deferred.resolve(container); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve container information', err: err }); - }); + Container.get({ id: id }) + .$promise.then(function success(data) { + var container = new ContainerDetailsViewModel(data); + deferred.resolve(container); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve container information', err: err }); + }); - return deferred.promise; - }; + return deferred.promise; + }; - service.containers = function(all, filters) { - var deferred = $q.defer(); - Container.query({ all : all, filters: filters }).$promise - .then(function success(data) { - var containers = data.map(function (item) { - return new ContainerViewModel(item); - }); - deferred.resolve(containers); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve containers', err: err }); - }); + service.containers = function (all, filters) { + var deferred = $q.defer(); + Container.query({ all: all, filters: filters }) + .$promise.then(function success(data) { + var containers = data.map(function (item) { + return new ContainerViewModel(item); + }); + deferred.resolve(containers); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve containers', err: err }); + }); - return deferred.promise; - }; + return deferred.promise; + }; - service.resizeTTY = function (id, width, height, timeout) { - var deferred = $q.defer(); + service.resizeTTY = function (id, width, height, timeout) { + var deferred = $q.defer(); - $timeout(function() { - Container.resize({}, {id: id, height: height, width: width}).$promise - .then(function success(data) { + $timeout(function () { + Container.resize({}, { id: id, height: height, width: width }) + .$promise.then(function success(data) { if (data.message) { - deferred.reject({msg: 'Unable to resize tty of container ' + id, err: data.message}); + deferred.reject({ msg: 'Unable to resize tty of container ' + id, err: data.message }); } else { deferred.resolve(data); } }) .catch(function error(err) { - deferred.reject({msg: 'Unable to resize tty of container ' + id, err: err}); + deferred.reject({ msg: 'Unable to resize tty of container ' + id, err: err }); }); - }, timeout); + }, timeout); - return deferred.promise; - }; - - service.startContainer = function(id) { - return Container.start({ id: id }, {}).$promise; - }; - - service.stopContainer = function(id) { - return Container.stop({ id: id }, {}).$promise; - }; - - service.restartContainer = function(id) { - return Container.restart({ id: id }, {}).$promise; - }; - - service.killContainer = function(id) { - return Container.kill({ id: id }, {}).$promise; - }; - - service.pauseContainer = function(id) { - return Container.pause({ id: id }, {}).$promise; - }; - - service.resumeContainer = function(id) { - return Container.unpause({ id: id }, {}).$promise; - }; - - service.renameContainer = function(id, newContainerName) { - return Container.rename({id: id, name: newContainerName }, {}).$promise; - }; - - service.updateRestartPolicy = updateRestartPolicy; - - function updateRestartPolicy(id, restartPolicy, maximumRetryCounts) { - return Container.update({ id: id }, - { RestartPolicy: { Name: restartPolicy, MaximumRetryCount: maximumRetryCounts } } - ).$promise; - } - - service.createContainer = function(configuration) { - var deferred = $q.defer(); - Container.create(configuration).$promise - .then(function success(data) { - deferred.resolve(data); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to create container', err: err }); - }); - return deferred.promise; - }; - - service.createAndStartContainer = function(configuration) { - var deferred = $q.defer(); - var container; - service.createContainer(configuration) - .then(function success(data) { - container = data; - return service.startContainer(container.Id); - }) - .then(function success() { - deferred.resolve(container); - }) - .catch(function error(err) { - deferred.reject(err); - }); - return deferred.promise; - }; - - service.remove = function(container, removeVolumes) { - var deferred = $q.defer(); - - Container.remove({ id: container.Id, v: (removeVolumes) ? 1 : 0, force: true }).$promise - .then(function success(data) { - if (data.message) { - deferred.reject({ msg: data.message, err: data.message }); - } else { - deferred.resolve(); - } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to remove container', err: err }); - }); - - return deferred.promise; - }; - - service.createExec = function(execConfig) { - var deferred = $q.defer(); - - Container.exec({}, execConfig).$promise - .then(function success(data) { - if (data.message) { - deferred.reject({ msg: data.message, err: data.message }); - } else { - deferred.resolve(data); - } - }) - .catch(function error(err) { - deferred.reject(err); - }); - - return deferred.promise; - }; - - service.logs = function(id, stdout, stderr, timestamps, since, tail, stripHeaders) { - var deferred = $q.defer(); - - var parameters = { - id: id, - stdout: stdout || 0, - stderr: stderr || 0, - timestamps: timestamps || 0, - since: since || 0, - tail: tail || 'all' + return deferred.promise; }; - Container.logs(parameters).$promise - .then(function success(data) { - var logs = LogHelper.formatLogs(data.logs, stripHeaders); - deferred.resolve(logs); - }) - .catch(function error(err) { - deferred.reject(err); - }); + service.startContainer = function (id) { + return Container.start({ id: id }, {}).$promise; + }; - return deferred.promise; - }; + service.stopContainer = function (id) { + return Container.stop({ id: id }, {}).$promise; + }; - service.containerStats = function(id) { - var deferred = $q.defer(); + service.restartContainer = function (id) { + return Container.restart({ id: id }, {}).$promise; + }; - Container.stats({ id: id }).$promise - .then(function success(data) { - var containerStats = new ContainerStatsViewModel(data); - deferred.resolve(containerStats); - }) - .catch(function error(err) { - deferred.reject(err); - }); + service.killContainer = function (id) { + return Container.kill({ id: id }, {}).$promise; + }; - return deferred.promise; - }; + service.pauseContainer = function (id) { + return Container.pause({ id: id }, {}).$promise; + }; - service.containerTop = function(id) { - return Container.top({ id: id }).$promise; - }; + service.resumeContainer = function (id) { + return Container.unpause({ id: id }, {}).$promise; + }; - service.inspect = function(id) { - return Container.inspect({ id: id }).$promise; - }; + service.renameContainer = function (id, newContainerName) { + return Container.rename({ id: id, name: newContainerName }, {}).$promise; + }; - service.prune = function(filters) { - return Container.prune({ filters: filters }).$promise; - }; + service.updateRestartPolicy = updateRestartPolicy; - return service; -}]); + function updateRestartPolicy(id, restartPolicy, maximumRetryCounts) { + return Container.update({ id: id }, { RestartPolicy: { Name: restartPolicy, MaximumRetryCount: maximumRetryCounts } }).$promise; + } + + service.createContainer = function (configuration) { + var deferred = $q.defer(); + Container.create(configuration) + .$promise.then(function success(data) { + deferred.resolve(data); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to create container', err: err }); + }); + return deferred.promise; + }; + + service.createAndStartContainer = function (configuration) { + var deferred = $q.defer(); + var container; + service + .createContainer(configuration) + .then(function success(data) { + container = data; + return service.startContainer(container.Id); + }) + .then(function success() { + deferred.resolve(container); + }) + .catch(function error(err) { + deferred.reject(err); + }); + return deferred.promise; + }; + + service.remove = function (container, removeVolumes) { + var deferred = $q.defer(); + + Container.remove({ id: container.Id, v: removeVolumes ? 1 : 0, force: true }) + .$promise.then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message, err: data.message }); + } else { + deferred.resolve(); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to remove container', err: err }); + }); + + return deferred.promise; + }; + + service.createExec = function (execConfig) { + var deferred = $q.defer(); + + Container.exec({}, execConfig) + .$promise.then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message, err: data.message }); + } else { + deferred.resolve(data); + } + }) + .catch(function error(err) { + deferred.reject(err); + }); + + return deferred.promise; + }; + + service.logs = function (id, stdout, stderr, timestamps, since, tail, stripHeaders) { + var deferred = $q.defer(); + + var parameters = { + id: id, + stdout: stdout || 0, + stderr: stderr || 0, + timestamps: timestamps || 0, + since: since || 0, + tail: tail || 'all', + }; + + Container.logs(parameters) + .$promise.then(function success(data) { + var logs = LogHelper.formatLogs(data.logs, stripHeaders); + deferred.resolve(logs); + }) + .catch(function error(err) { + deferred.reject(err); + }); + + return deferred.promise; + }; + + service.containerStats = function (id) { + var deferred = $q.defer(); + + Container.stats({ id: id }) + .$promise.then(function success(data) { + var containerStats = new ContainerStatsViewModel(data); + deferred.resolve(containerStats); + }) + .catch(function error(err) { + deferred.reject(err); + }); + + return deferred.promise; + }; + + service.containerTop = function (id) { + return Container.top({ id: id }).$promise; + }; + + service.inspect = function (id) { + return Container.inspect({ id: id }).$promise; + }; + + service.prune = function (filters) { + return Container.prune({ filters: filters }).$promise; + }; + + return service; + }, +]); diff --git a/app/docker/services/execService.js b/app/docker/services/execService.js index bcb09b77c..a6a29490f 100644 --- a/app/docker/services/execService.js +++ b/app/docker/services/execService.js @@ -1,27 +1,31 @@ -angular.module('portainer.docker') - .factory('ExecService', ['$q', '$timeout', 'Exec', function ExecServiceFactory($q, $timeout, Exec) { - 'use strict'; - var service = {}; +angular.module('portainer.docker').factory('ExecService', [ + '$q', + '$timeout', + 'Exec', + function ExecServiceFactory($q, $timeout, Exec) { + 'use strict'; + var service = {}; - service.resizeTTY = function (execId, width, height, timeout) { - var deferred = $q.defer(); + service.resizeTTY = function (execId, width, height, timeout) { + var deferred = $q.defer(); - $timeout(function() { - Exec.resize({}, {id: execId, height: height, width: width}).$promise - .then(function success(data) { - if (data.message) { - deferred.reject({msg: "Unable to resize tty of exec", err: data.message}); - } else { - deferred.resolve(data); - } - }) - .catch(function error(err) { - deferred.reject({msg: "Unable to resize tty of exec", err: err}); - }); - }, timeout); + $timeout(function () { + Exec.resize({}, { id: execId, height: height, width: width }) + .$promise.then(function success(data) { + if (data.message) { + deferred.reject({ msg: 'Unable to resize tty of exec', err: data.message }); + } else { + deferred.resolve(data); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to resize tty of exec', err: err }); + }); + }, timeout); - return deferred.promise; - }; + return deferred.promise; + }; - return service; - }]); + return service; + }, +]); diff --git a/app/docker/services/imageService.js b/app/docker/services/imageService.js index afd70aff3..56a13983e 100644 --- a/app/docker/services/imageService.js +++ b/app/docker/services/imageService.js @@ -1,205 +1,212 @@ import _ from 'lodash-es'; import { ImageViewModel } from '../models/image'; -import { ImageDetailsViewModel } from "../models/imageDetails"; -import { ImageLayerViewModel } from "../models/imageLayer"; +import { ImageDetailsViewModel } from '../models/imageDetails'; +import { ImageLayerViewModel } from '../models/imageLayer'; -angular.module('portainer.docker') -.factory('ImageService', ['$q', 'Image', 'ImageHelper', 'RegistryService', 'HttpRequestHelper', 'ContainerService', 'FileUploadService', +angular.module('portainer.docker').factory('ImageService', [ + '$q', + 'Image', + 'ImageHelper', + 'RegistryService', + 'HttpRequestHelper', + 'ContainerService', + 'FileUploadService', function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper, ContainerService, FileUploadService) { - 'use strict'; - var service = {}; + 'use strict'; + var service = {}; - service.image = function(imageId) { - var deferred = $q.defer(); - Image.get({id: imageId}).$promise - .then(function success(data) { - if (data.message) { - deferred.reject({ msg: data.message }); - } else { - var image = new ImageDetailsViewModel(data); - deferred.resolve(image); - } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve image details', err: err }); - }); - return deferred.promise; - }; - - service.images = function(withUsage) { - var deferred = $q.defer(); - - $q.all({ - containers: withUsage ? ContainerService.containers(1) : [], - images: Image.query({}).$promise - }) - .then(function success(data) { - var containers = data.containers; - - var images = data.images.map(function(item) { - item.ContainerCount = 0; - for (var i = 0; i < containers.length; i++) { - var container = containers[i]; - if (container.ImageID === item.Id) { - item.ContainerCount++; + service.image = function (imageId) { + var deferred = $q.defer(); + Image.get({ id: imageId }) + .$promise.then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message }); + } else { + var image = new ImageDetailsViewModel(data); + deferred.resolve(image); } - } - return new ImageViewModel(item); - }); - - deferred.resolve(images); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve images', err: err }); - }); - return deferred.promise; - }; - - service.history = function(imageId) { - var deferred = $q.defer(); - Image.history({id: imageId}).$promise - .then(function success(data) { - if (data.message) { - deferred.reject({ msg: data.message }); - } else { - var layers = []; - var order = data.length; - angular.forEach(data, function(imageLayer) { - layers.push(new ImageLayerViewModel(order, imageLayer)); - order--; + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve image details', err: err }); }); - deferred.resolve(layers); - } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve image details', err: err }); - }); - return deferred.promise; - }; + return deferred.promise; + }; - service.pushImage = pushImage; - /** - * - * @param {PorImageRegistryModel} registryModel - */ - function pushImage(registryModel) { - var deferred = $q.defer(); + service.images = function (withUsage) { + var deferred = $q.defer(); - var authenticationDetails = registryModel.Registry.Authentication ? RegistryService.encodedCredentials(registryModel.Registry) : ''; - HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); + $q.all({ + containers: withUsage ? ContainerService.containers(1) : [], + images: Image.query({}).$promise, + }) + .then(function success(data) { + var containers = data.containers; - const imageConfiguration = ImageHelper.createImageConfigForContainer(registryModel); + var images = data.images.map(function (item) { + item.ContainerCount = 0; + for (var i = 0; i < containers.length; i++) { + var container = containers[i]; + if (container.ImageID === item.Id) { + item.ContainerCount++; + } + } + return new ImageViewModel(item); + }); - Image.push({imageName: imageConfiguration.fromImage}).$promise - .then(function success(data) { - if (data[data.length - 1].error) { - deferred.reject({ msg: data[data.length - 1].error }); - } else { - deferred.resolve(); - } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to push image tag', err: err }); - }); - return deferred.promise; - } + deferred.resolve(images); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve images', err: err }); + }); + return deferred.promise; + }; - /** - * PULL IMAGE - */ + service.history = function (imageId) { + var deferred = $q.defer(); + Image.history({ id: imageId }) + .$promise.then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message }); + } else { + var layers = []; + var order = data.length; + angular.forEach(data, function (imageLayer) { + layers.push(new ImageLayerViewModel(order, imageLayer)); + order--; + }); + deferred.resolve(layers); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve image details', err: err }); + }); + return deferred.promise; + }; - function pullImageAndIgnoreErrors(imageConfiguration) { - var deferred = $q.defer(); + service.pushImage = pushImage; + /** + * + * @param {PorImageRegistryModel} registryModel + */ + function pushImage(registryModel) { + var deferred = $q.defer(); - Image.create({}, imageConfiguration).$promise - .finally(function final() { - deferred.resolve(); - }); + var authenticationDetails = registryModel.Registry.Authentication ? RegistryService.encodedCredentials(registryModel.Registry) : ''; + HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); - return deferred.promise; - } + const imageConfiguration = ImageHelper.createImageConfigForContainer(registryModel); - function pullImageAndAcknowledgeErrors(imageConfiguration) { - var deferred = $q.defer(); - - Image.create({}, imageConfiguration).$promise - .then(function success(data) { - var err = data.length > 0 && data[data.length - 1].hasOwnProperty('message'); - if (err) { - var detail = data[data.length - 1]; - deferred.reject({ msg: detail.message }); - } else { - deferred.resolve(data); - } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to pull image', err: err }); - }); - - return deferred.promise; - } - - service.pullImage = pullImage; - - /** - * - * @param {PorImageRegistryModel} registry - * @param {bool} ignoreErrors - */ - function pullImage(registry, ignoreErrors) { - var authenticationDetails = registry.Registry.Authentication ? RegistryService.encodedCredentials(registry.Registry) : ''; - HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); - - var imageConfiguration = ImageHelper.createImageConfigForContainer(registry); - - if (ignoreErrors) { - return pullImageAndIgnoreErrors(imageConfiguration); + Image.push({ imageName: imageConfiguration.fromImage }) + .$promise.then(function success(data) { + if (data[data.length - 1].error) { + deferred.reject({ msg: data[data.length - 1].error }); + } else { + deferred.resolve(); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to push image tag', err: err }); + }); + return deferred.promise; } - return pullImageAndAcknowledgeErrors(imageConfiguration); - } - /** - * ! PULL IMAGE - */ + /** + * PULL IMAGE + */ - service.tagImage = function(id, image) { - return Image.tag({id: id, repo: image}).$promise; - }; + function pullImageAndIgnoreErrors(imageConfiguration) { + var deferred = $q.defer(); - service.downloadImages = function(images) { - var names = ImageHelper.getImagesNamesForDownload(images); - return Image.download(names).$promise; - }; - - service.uploadImage = function(file) { - return FileUploadService.loadImages(file); - }; - - service.deleteImage = function(id, forceRemoval) { - var deferred = $q.defer(); - Image.remove({id: id, force: forceRemoval}).$promise - .then(function success(data) { - if (data[0].message) { - deferred.reject({ msg: data[0].message }); - } else { + Image.create({}, imageConfiguration).$promise.finally(function final() { deferred.resolve(); - } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to remove image', err: err }); - }); - return deferred.promise; - }; - - service.getUniqueTagListFromImages = function (availableImages) { - - return _.uniq(_.flatMap(availableImages, function (image) { - _.remove(image.RepoTags, function (item) { - return item.indexOf('') !== -1; }); - return image.RepoTags ? _.uniqWith(image.RepoTags, _.isEqual) : []; - })); - }; - return service; -}]); + return deferred.promise; + } + + function pullImageAndAcknowledgeErrors(imageConfiguration) { + var deferred = $q.defer(); + + Image.create({}, imageConfiguration) + .$promise.then(function success(data) { + var err = data.length > 0 && data[data.length - 1].hasOwnProperty('message'); + if (err) { + var detail = data[data.length - 1]; + deferred.reject({ msg: detail.message }); + } else { + deferred.resolve(data); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to pull image', err: err }); + }); + + return deferred.promise; + } + + service.pullImage = pullImage; + + /** + * + * @param {PorImageRegistryModel} registry + * @param {bool} ignoreErrors + */ + function pullImage(registry, ignoreErrors) { + var authenticationDetails = registry.Registry.Authentication ? RegistryService.encodedCredentials(registry.Registry) : ''; + HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); + + var imageConfiguration = ImageHelper.createImageConfigForContainer(registry); + + if (ignoreErrors) { + return pullImageAndIgnoreErrors(imageConfiguration); + } + return pullImageAndAcknowledgeErrors(imageConfiguration); + } + + /** + * ! PULL IMAGE + */ + + service.tagImage = function (id, image) { + return Image.tag({ id: id, repo: image }).$promise; + }; + + service.downloadImages = function (images) { + var names = ImageHelper.getImagesNamesForDownload(images); + return Image.download(names).$promise; + }; + + service.uploadImage = function (file) { + return FileUploadService.loadImages(file); + }; + + service.deleteImage = function (id, forceRemoval) { + var deferred = $q.defer(); + Image.remove({ id: id, force: forceRemoval }) + .$promise.then(function success(data) { + if (data[0].message) { + deferred.reject({ msg: data[0].message }); + } else { + deferred.resolve(); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to remove image', err: err }); + }); + return deferred.promise; + }; + + service.getUniqueTagListFromImages = function (availableImages) { + return _.uniq( + _.flatMap(availableImages, function (image) { + _.remove(image.RepoTags, function (item) { + return item.indexOf('') !== -1; + }); + return image.RepoTags ? _.uniqWith(image.RepoTags, _.isEqual) : []; + }) + ); + }; + + return service; + }, +]); diff --git a/app/docker/services/networkService.js b/app/docker/services/networkService.js index f8400b693..3804aaeed 100644 --- a/app/docker/services/networkService.js +++ b/app/docker/services/networkService.js @@ -1,87 +1,92 @@ import { NetworkViewModel } from '../models/network'; -angular.module('portainer.docker') -.factory('NetworkService', ['$q', 'Network', function NetworkServiceFactory($q, Network) { - 'use strict'; - var service = {}; +angular.module('portainer.docker').factory('NetworkService', [ + '$q', + 'Network', + function NetworkServiceFactory($q, Network) { + 'use strict'; + var service = {}; - service.create = function(networkConfiguration) { - var deferred = $q.defer(); + service.create = function (networkConfiguration) { + var deferred = $q.defer(); - Network.create(networkConfiguration).$promise - .then(function success(data) { - deferred.resolve(data); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to create network', err: err }); - }); - return deferred.promise; - }; - - service.network = function(id) { - var deferred = $q.defer(); - - Network.get({ id: id }).$promise - .then(function success(data) { - var network = new NetworkViewModel(data); - deferred.resolve(network); - }) - .catch(function error(err) { - deferred.reject({msg: 'Unable to retrieve network details', err: err}); - }); - - return deferred.promise; - }; - - service.networks = function(localNetworks, swarmNetworks, swarmAttachableNetworks, filters) { - var deferred = $q.defer(); - - Network.query({ filters: filters }).$promise - .then(function success(data) { - var networks = data; - - var filteredNetworks = networks.filter(function(network) { - if (localNetworks && network.Scope === 'local') { - return network; - } - if (swarmNetworks && network.Scope === 'swarm') { - return network; - } - if (swarmAttachableNetworks && network.Scope === 'swarm' && network.Attachable === true) { - return network; - } - }).map(function (item) { - return new NetworkViewModel(item); - }); - - deferred.resolve(filteredNetworks); - }) - .catch(function error(err) { - deferred.reject({msg: 'Unable to retrieve networks', err: err}); - }); - - return deferred.promise; - }; - - service.remove = function(id) { - return Network.remove({ id: id }).$promise; - }; - - service.disconnectContainer = function(networkId, containerId, force) { - return Network.disconnect({ id: networkId }, { Container: containerId, Force: force }).$promise; - }; - - service.connectContainer = function(networkId, containerId, aliases) { - var payload = { - Container: containerId, + Network.create(networkConfiguration) + .$promise.then(function success(data) { + deferred.resolve(data); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to create network', err: err }); + }); + return deferred.promise; }; - if (aliases) { - payload.EndpointConfig = { - Aliases: aliases, - } - } - return Network.connect({ id: networkId }, payload).$promise; - }; - return service; -}]); + service.network = function (id) { + var deferred = $q.defer(); + + Network.get({ id: id }) + .$promise.then(function success(data) { + var network = new NetworkViewModel(data); + deferred.resolve(network); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve network details', err: err }); + }); + + return deferred.promise; + }; + + service.networks = function (localNetworks, swarmNetworks, swarmAttachableNetworks, filters) { + var deferred = $q.defer(); + + Network.query({ filters: filters }) + .$promise.then(function success(data) { + var networks = data; + + var filteredNetworks = networks + .filter(function (network) { + if (localNetworks && network.Scope === 'local') { + return network; + } + if (swarmNetworks && network.Scope === 'swarm') { + return network; + } + if (swarmAttachableNetworks && network.Scope === 'swarm' && network.Attachable === true) { + return network; + } + }) + .map(function (item) { + return new NetworkViewModel(item); + }); + + deferred.resolve(filteredNetworks); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve networks', err: err }); + }); + + return deferred.promise; + }; + + service.remove = function (id) { + return Network.remove({ id: id }).$promise; + }; + + service.disconnectContainer = function (networkId, containerId, force) { + return Network.disconnect({ id: networkId }, { Container: containerId, Force: force }).$promise; + }; + + service.connectContainer = function (networkId, containerId, aliases) { + var payload = { + Container: containerId, + }; + if (aliases) { + payload.EndpointConfig = { + Aliases: aliases, + }; + } + return Network.connect({ id: networkId }, payload).$promise; + }; + + return service; + }, +]); diff --git a/app/docker/services/nodeService.js b/app/docker/services/nodeService.js index 943f4201d..42ddf7ed8 100644 --- a/app/docker/services/nodeService.js +++ b/app/docker/services/nodeService.js @@ -1,7 +1,8 @@ import { NodeViewModel } from '../models/node'; angular.module('portainer.docker').factory('NodeService', [ - '$q', 'Node', + '$q', + 'Node', function NodeServiceFactory($q, Node) { 'use strict'; var service = {}; @@ -30,7 +31,7 @@ angular.module('portainer.docker').factory('NodeService', [ Node.query({}) .$promise.then(function success(data) { - var nodes = data.map(function(item) { + var nodes = data.map(function (item) { return new NodeViewModel(item); }); deferred.resolve(nodes); @@ -49,7 +50,8 @@ angular.module('portainer.docker').factory('NodeService', [ function getActiveManager() { var deferred = $q.defer(); - service.nodes() + service + .nodes() .then(function success(data) { for (var i = 0; i < data.length; ++i) { var node = data[i]; @@ -67,5 +69,5 @@ angular.module('portainer.docker').factory('NodeService', [ } return service; - } + }, ]); diff --git a/app/docker/services/pluginService.js b/app/docker/services/pluginService.js index 49c6d2afb..cdf22b90a 100644 --- a/app/docker/services/pluginService.js +++ b/app/docker/services/pluginService.js @@ -1,72 +1,76 @@ import _ from 'lodash-es'; -import { PluginViewModel } from "../models/plugin"; +import { PluginViewModel } from '../models/plugin'; -angular.module('portainer.docker') -.factory('PluginService', ['$q', 'Plugin', 'SystemService', function PluginServiceFactory($q, Plugin, SystemService) { - 'use strict'; - var service = {}; +angular.module('portainer.docker').factory('PluginService', [ + '$q', + 'Plugin', + 'SystemService', + function PluginServiceFactory($q, Plugin, SystemService) { + 'use strict'; + var service = {}; - service.plugins = function() { - var deferred = $q.defer(); - var plugins = []; + service.plugins = function () { + var deferred = $q.defer(); + var plugins = []; - Plugin.query({}).$promise - .then(function success(data) { - for (var i = 0; i < data.length; i++) { - var plugin = new PluginViewModel(data[i]); - plugins.push(plugin); - } - }) - .finally(function final() { - deferred.resolve(plugins); - }); + Plugin.query({}) + .$promise.then(function success(data) { + for (var i = 0; i < data.length; i++) { + var plugin = new PluginViewModel(data[i]); + plugins.push(plugin); + } + }) + .finally(function final() { + deferred.resolve(plugins); + }); - return deferred.promise; - }; + return deferred.promise; + }; - function servicePlugins(systemOnly, pluginType, pluginVersion) { - var deferred = $q.defer(); + function servicePlugins(systemOnly, pluginType, pluginVersion) { + var deferred = $q.defer(); - $q.all({ - system: SystemService.plugins(), - plugins: systemOnly ? [] : service.plugins() - }) - .then(function success(data) { - var aggregatedPlugins = []; - var systemPlugins = data.system; - var plugins = data.plugins; + $q.all({ + system: SystemService.plugins(), + plugins: systemOnly ? [] : service.plugins(), + }) + .then(function success(data) { + var aggregatedPlugins = []; + var systemPlugins = data.system; + var plugins = data.plugins; - if (systemPlugins[pluginType]) { - aggregatedPlugins = aggregatedPlugins.concat(systemPlugins[pluginType]); - } + if (systemPlugins[pluginType]) { + aggregatedPlugins = aggregatedPlugins.concat(systemPlugins[pluginType]); + } - for (var i = 0; i < plugins.length; i++) { - var plugin = plugins[i]; - if (plugin.Enabled && _.includes(plugin.Config.Interface.Types, pluginVersion)) { - aggregatedPlugins.push(plugin.Name); - } - } + for (var i = 0; i < plugins.length; i++) { + var plugin = plugins[i]; + if (plugin.Enabled && _.includes(plugin.Config.Interface.Types, pluginVersion)) { + aggregatedPlugins.push(plugin.Name); + } + } - deferred.resolve(aggregatedPlugins); - }) - .catch(function error(err) { - deferred.reject({ msg: err.msg, err: err }); - }); + deferred.resolve(aggregatedPlugins); + }) + .catch(function error(err) { + deferred.reject({ msg: err.msg, err: err }); + }); - return deferred.promise; - } + return deferred.promise; + } - service.volumePlugins = function(systemOnly) { - return servicePlugins(systemOnly, 'Volume', 'docker.volumedriver/1.0'); - }; + service.volumePlugins = function (systemOnly) { + return servicePlugins(systemOnly, 'Volume', 'docker.volumedriver/1.0'); + }; - service.networkPlugins = function(systemOnly) { - return servicePlugins(systemOnly, 'Network', 'docker.networkdriver/1.0'); - }; + service.networkPlugins = function (systemOnly) { + return servicePlugins(systemOnly, 'Network', 'docker.networkdriver/1.0'); + }; - service.loggingPlugins = function(systemOnly) { - return servicePlugins(systemOnly, 'Log', 'docker.logdriver/1.0'); - }; + service.loggingPlugins = function (systemOnly) { + return servicePlugins(systemOnly, 'Log', 'docker.logdriver/1.0'); + }; - return service; -}]); + return service; + }, +]); diff --git a/app/docker/services/secretService.js b/app/docker/services/secretService.js index f12494929..40268c3fb 100644 --- a/app/docker/services/secretService.js +++ b/app/docker/services/secretService.js @@ -1,63 +1,66 @@ import { SecretViewModel } from '../models/secret'; -angular.module('portainer.docker') -.factory('SecretService', ['$q', 'Secret', function SecretServiceFactory($q, Secret) { - 'use strict'; - var service = {}; +angular.module('portainer.docker').factory('SecretService', [ + '$q', + 'Secret', + function SecretServiceFactory($q, Secret) { + 'use strict'; + var service = {}; - service.secret = function(secretId) { - var deferred = $q.defer(); + service.secret = function (secretId) { + var deferred = $q.defer(); - Secret.get({id: secretId}).$promise - .then(function success(data) { - var secret = new SecretViewModel(data); - deferred.resolve(secret); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve secret details', err: err }); - }); + Secret.get({ id: secretId }) + .$promise.then(function success(data) { + var secret = new SecretViewModel(data); + deferred.resolve(secret); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve secret details', err: err }); + }); - return deferred.promise; - }; + return deferred.promise; + }; - service.secrets = function() { - var deferred = $q.defer(); + service.secrets = function () { + var deferred = $q.defer(); - Secret.query({}).$promise - .then(function success(data) { - var secrets = data.map(function (item) { - return new SecretViewModel(item); - }); - deferred.resolve(secrets); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve secrets', err: err }); - }); + Secret.query({}) + .$promise.then(function success(data) { + var secrets = data.map(function (item) { + return new SecretViewModel(item); + }); + deferred.resolve(secrets); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve secrets', err: err }); + }); - return deferred.promise; - }; + return deferred.promise; + }; - service.remove = function(secretId) { - var deferred = $q.defer(); + service.remove = function (secretId) { + var deferred = $q.defer(); - Secret.remove({ id: secretId }).$promise - .then(function success(data) { - if (data.message) { - deferred.reject({ msg: data.message }); - } else { - deferred.resolve(); - } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to remove secret', err: err }); - }); + Secret.remove({ id: secretId }) + .$promise.then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message }); + } else { + deferred.resolve(); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to remove secret', err: err }); + }); - return deferred.promise; - }; + return deferred.promise; + }; - service.create = function(secretConfig) { - return Secret.create(secretConfig).$promise; - }; + service.create = function (secretConfig) { + return Secret.create(secretConfig).$promise; + }; - return service; -}]); + return service; + }, +]); diff --git a/app/docker/services/serviceService.js b/app/docker/services/serviceService.js index 95f16d214..d2caa7e69 100644 --- a/app/docker/services/serviceService.js +++ b/app/docker/services/serviceService.js @@ -1,98 +1,103 @@ import { ServiceViewModel } from '../models/service'; +angular.module('portainer.docker').factory('ServiceService', [ + '$q', + 'Service', + 'ServiceHelper', + 'TaskService', + 'ResourceControlService', + 'LogHelper', + function ServiceServiceFactory($q, Service, ServiceHelper, TaskService, ResourceControlService, LogHelper) { + 'use strict'; + var service = {}; -angular.module('portainer.docker') -.factory('ServiceService', ['$q', 'Service', 'ServiceHelper', 'TaskService', 'ResourceControlService', 'LogHelper', -function ServiceServiceFactory($q, Service, ServiceHelper, TaskService, ResourceControlService, LogHelper) { - 'use strict'; - var service = {}; + service.services = function (filters) { + var deferred = $q.defer(); - service.services = function(filters) { - var deferred = $q.defer(); + Service.query({ filters: filters ? filters : {} }) + .$promise.then(function success(data) { + var services = data.map(function (item) { + return new ServiceViewModel(item); + }); + deferred.resolve(services); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve services', err: err }); + }); - Service.query({ filters: filters ? filters : {} }).$promise - .then(function success(data) { - var services = data.map(function (item) { - return new ServiceViewModel(item); - }); - deferred.resolve(services); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve services', err: err }); - }); - - return deferred.promise; - }; - - service.service = function(id) { - var deferred = $q.defer(); - - Service.get({ id: id }).$promise - .then(function success(data) { - var service = new ServiceViewModel(data); - deferred.resolve(service); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve service details', err: err }); - }); - - return deferred.promise; - }; - - service.remove = function(service) { - var deferred = $q.defer(); - - Service.remove({id: service.Id}).$promise - .then(function success(data) { - if (data.message) { - deferred.reject({ msg: data.message, err: data.message }); - } else { - deferred.resolve(); - } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to remove service', err: err }); - }); - - return deferred.promise; - }; - - service.update = function(serv, config, rollback) { - return service.service(serv.Id).then((data) => { - const params = { - id: serv.Id, - version: data.Version - }; - if (rollback) { - params.rollback = rollback - } - return Service.update(params, config).$promise; - }); - }; - - service.logs = function(id, stdout, stderr, timestamps, since, tail) { - var deferred = $q.defer(); - - var parameters = { - id: id, - stdout: stdout || 0, - stderr: stderr || 0, - timestamps: timestamps || 0, - since: since || 0, - tail: tail || 'all' + return deferred.promise; }; - Service.logs(parameters).$promise - .then(function success(data) { - var logs = LogHelper.formatLogs(data.logs, true); - deferred.resolve(logs); - }) - .catch(function error(err) { - deferred.reject(err); - }); + service.service = function (id) { + var deferred = $q.defer(); - return deferred.promise; - }; + Service.get({ id: id }) + .$promise.then(function success(data) { + var service = new ServiceViewModel(data); + deferred.resolve(service); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve service details', err: err }); + }); - return service; -}]); + return deferred.promise; + }; + + service.remove = function (service) { + var deferred = $q.defer(); + + Service.remove({ id: service.Id }) + .$promise.then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message, err: data.message }); + } else { + deferred.resolve(); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to remove service', err: err }); + }); + + return deferred.promise; + }; + + service.update = function (serv, config, rollback) { + return service.service(serv.Id).then((data) => { + const params = { + id: serv.Id, + version: data.Version, + }; + if (rollback) { + params.rollback = rollback; + } + return Service.update(params, config).$promise; + }); + }; + + service.logs = function (id, stdout, stderr, timestamps, since, tail) { + var deferred = $q.defer(); + + var parameters = { + id: id, + stdout: stdout || 0, + stderr: stderr || 0, + timestamps: timestamps || 0, + since: since || 0, + tail: tail || 'all', + }; + + Service.logs(parameters) + .$promise.then(function success(data) { + var logs = LogHelper.formatLogs(data.logs, true); + deferred.resolve(logs); + }) + .catch(function error(err) { + deferred.reject(err); + }); + + return deferred.promise; + }; + + return service; + }, +]); diff --git a/app/docker/services/swarmService.js b/app/docker/services/swarmService.js index 308163ef5..9ad18d212 100644 --- a/app/docker/services/swarmService.js +++ b/app/docker/services/swarmService.js @@ -1,24 +1,27 @@ import { SwarmViewModel } from '../models/swarm'; -angular.module('portainer.docker') -.factory('SwarmService', ['$q', 'Swarm', function SwarmServiceFactory($q, Swarm) { - 'use strict'; - var service = {}; +angular.module('portainer.docker').factory('SwarmService', [ + '$q', + 'Swarm', + function SwarmServiceFactory($q, Swarm) { + 'use strict'; + var service = {}; - service.swarm = function() { - var deferred = $q.defer(); + service.swarm = function () { + var deferred = $q.defer(); - Swarm.get().$promise - .then(function success(data) { - var swarm = new SwarmViewModel(data); - deferred.resolve(swarm); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve Swarm details', err: err }); - }); + Swarm.get() + .$promise.then(function success(data) { + var swarm = new SwarmViewModel(data); + deferred.resolve(swarm); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve Swarm details', err: err }); + }); - return deferred.promise; - }; + return deferred.promise; + }; - return service; -}]); + return service; + }, +]); diff --git a/app/docker/services/systemService.js b/app/docker/services/systemService.js index 9e89300ce..90ac1168e 100644 --- a/app/docker/services/systemService.js +++ b/app/docker/services/systemService.js @@ -1,55 +1,59 @@ -import {EventViewModel} from '../models/event'; +import { EventViewModel } from '../models/event'; -angular.module('portainer.docker') -.factory('SystemService', ['$q', 'System', 'SystemEndpoint', function SystemServiceFactory($q, System, SystemEndpoint) { - 'use strict'; - var service = {}; +angular.module('portainer.docker').factory('SystemService', [ + '$q', + 'System', + 'SystemEndpoint', + function SystemServiceFactory($q, System, SystemEndpoint) { + 'use strict'; + var service = {}; - service.plugins = function() { - var deferred = $q.defer(); - System.info({}).$promise - .then(function success(data) { - var plugins = data.Plugins; - deferred.resolve(plugins); - }) - .catch(function error(err) { - deferred.reject({msg: 'Unable to retrieve plugins information from system', err: err}); - }); - return deferred.promise; - }; + service.plugins = function () { + var deferred = $q.defer(); + System.info({}) + .$promise.then(function success(data) { + var plugins = data.Plugins; + deferred.resolve(plugins); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve plugins information from system', err: err }); + }); + return deferred.promise; + }; - service.info = function() { - return System.info({}).$promise; - }; + service.info = function () { + return System.info({}).$promise; + }; - service.ping = function(endpointId) { - return SystemEndpoint.ping({endpointId: endpointId}).$promise; - }; + service.ping = function (endpointId) { + return SystemEndpoint.ping({ endpointId: endpointId }).$promise; + }; - service.version = function() { - return System.version({}).$promise; - }; + service.version = function () { + return System.version({}).$promise; + }; - service.events = function(from, to) { - var deferred = $q.defer(); + service.events = function (from, to) { + var deferred = $q.defer(); - System.events({since: from, until: to}).$promise - .then(function success(data) { - var events = data.map(function (item) { - return new EventViewModel(item); - }); - deferred.resolve(events); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve engine events', err: err }); - }); + System.events({ since: from, until: to }) + .$promise.then(function success(data) { + var events = data.map(function (item) { + return new EventViewModel(item); + }); + deferred.resolve(events); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve engine events', err: err }); + }); - return deferred.promise; - }; + return deferred.promise; + }; - service.dataUsage = function () { - return System.dataUsage().$promise; - }; + service.dataUsage = function () { + return System.dataUsage().$promise; + }; - return service; -}]); + return service; + }, +]); diff --git a/app/docker/services/taskService.js b/app/docker/services/taskService.js index 472ca051c..e5e6d4708 100644 --- a/app/docker/services/taskService.js +++ b/app/docker/services/taskService.js @@ -1,66 +1,69 @@ import { TaskViewModel } from '../models/task'; -angular.module('portainer.docker') -.factory('TaskService', ['$q', 'Task', 'LogHelper', -function TaskServiceFactory($q, Task, LogHelper) { - 'use strict'; - var service = {}; +angular.module('portainer.docker').factory('TaskService', [ + '$q', + 'Task', + 'LogHelper', + function TaskServiceFactory($q, Task, LogHelper) { + 'use strict'; + var service = {}; - service.task = function(id) { - var deferred = $q.defer(); + service.task = function (id) { + var deferred = $q.defer(); - Task.get({ id: id }).$promise - .then(function success(data) { - var task = new TaskViewModel(data); - deferred.resolve(task); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve task details', err: err }); - }); + Task.get({ id: id }) + .$promise.then(function success(data) { + var task = new TaskViewModel(data); + deferred.resolve(task); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve task details', err: err }); + }); - return deferred.promise; - }; - - service.tasks = function(filters) { - var deferred = $q.defer(); - - Task.query({ filters: filters ? filters : {} }).$promise - .then(function success(data) { - var tasks = data.map(function (item) { - return new TaskViewModel(item); - }); - deferred.resolve(tasks); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve tasks', err: err }); - }); - - return deferred.promise; - }; - - service.logs = function(id, stdout, stderr, timestamps, since, tail) { - var deferred = $q.defer(); - - var parameters = { - id: id, - stdout: stdout || 0, - stderr: stderr || 0, - timestamps: timestamps || 0, - since: since || 0, - tail: tail || 'all' + return deferred.promise; }; - Task.logs(parameters).$promise - .then(function success(data) { - var logs = LogHelper.formatLogs(data.logs, true); - deferred.resolve(logs); - }) - .catch(function error(err) { - deferred.reject(err); - }); + service.tasks = function (filters) { + var deferred = $q.defer(); - return deferred.promise; - }; + Task.query({ filters: filters ? filters : {} }) + .$promise.then(function success(data) { + var tasks = data.map(function (item) { + return new TaskViewModel(item); + }); + deferred.resolve(tasks); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve tasks', err: err }); + }); - return service; -}]); + return deferred.promise; + }; + + service.logs = function (id, stdout, stderr, timestamps, since, tail) { + var deferred = $q.defer(); + + var parameters = { + id: id, + stdout: stdout || 0, + stderr: stderr || 0, + timestamps: timestamps || 0, + since: since || 0, + tail: tail || 'all', + }; + + Task.logs(parameters) + .$promise.then(function success(data) { + var logs = LogHelper.formatLogs(data.logs, true); + deferred.resolve(logs); + }) + .catch(function error(err) { + deferred.reject(err); + }); + + return deferred.promise; + }; + + return service; + }, +]); diff --git a/app/docker/services/volumeService.js b/app/docker/services/volumeService.js index 1ad5cbb9c..a812a86cb 100644 --- a/app/docker/services/volumeService.js +++ b/app/docker/services/volumeService.js @@ -1,101 +1,105 @@ import { VolumeViewModel } from '../models/volume'; -angular.module('portainer.docker') -.factory('VolumeService', ['$q', 'Volume', 'VolumeHelper', function VolumeServiceFactory($q, Volume, VolumeHelper) { - 'use strict'; - var service = {}; +angular.module('portainer.docker').factory('VolumeService', [ + '$q', + 'Volume', + 'VolumeHelper', + function VolumeServiceFactory($q, Volume, VolumeHelper) { + 'use strict'; + var service = {}; - service.volumes = function(params) { - var deferred = $q.defer(); - Volume.query(params).$promise - .then(function success(data) { - var volumes = data.Volumes || []; - volumes = volumes.map(function (item) { - return new VolumeViewModel(item); - }); - deferred.resolve(volumes); - }) - .catch(function error(err) { - deferred.reject({msg: 'Unable to retrieve volumes', err: err}); - }); - return deferred.promise; - }; - - service.volume = function(id) { - var deferred = $q.defer(); - Volume.get({id: id}).$promise - .then(function success(data) { - var volume = new VolumeViewModel(data); - deferred.resolve(volume); - }) - .catch(function error(err) { - deferred.reject({msg: 'Unable to retrieve volume details', err: err}); - }); - return deferred.promise; - }; - - service.getVolumes = function() { - return Volume.query({}).$promise; - }; - - service.remove = function(volume) { - var deferred = $q.defer(); - - Volume.remove({id: volume.Id}).$promise - .then(function success(data) { - if (data.message) { - deferred.reject({ msg: data.message, err: data.message }); - } else { - deferred.resolve(); - } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to remove volume', err: err }); - }); - - return deferred.promise; - }; - - service.createVolumeConfiguration = function(name, driver, driverOptions) { - var volumeConfiguration = { - Name: name, - Driver: driver, - DriverOpts: VolumeHelper.createDriverOptions(driverOptions) + service.volumes = function (params) { + var deferred = $q.defer(); + Volume.query(params) + .$promise.then(function success(data) { + var volumes = data.Volumes || []; + volumes = volumes.map(function (item) { + return new VolumeViewModel(item); + }); + deferred.resolve(volumes); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve volumes', err: err }); + }); + return deferred.promise; }; - return volumeConfiguration; - }; - service.createVolume = function(volumeConfiguration) { - var deferred = $q.defer(); - Volume.create(volumeConfiguration).$promise - .then(function success(data) { - if (data.message) { - deferred.reject({ msg: data.message }); - } else { - var volume = new VolumeViewModel(data); - deferred.resolve(volume); + service.volume = function (id) { + var deferred = $q.defer(); + Volume.get({ id: id }) + .$promise.then(function success(data) { + var volume = new VolumeViewModel(data); + deferred.resolve(volume); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve volume details', err: err }); + }); + return deferred.promise; + }; + + service.getVolumes = function () { + return Volume.query({}).$promise; + }; + + service.remove = function (volume) { + var deferred = $q.defer(); + + Volume.remove({ id: volume.Id }) + .$promise.then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message, err: data.message }); + } else { + deferred.resolve(); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to remove volume', err: err }); + }); + + return deferred.promise; + }; + + service.createVolumeConfiguration = function (name, driver, driverOptions) { + var volumeConfiguration = { + Name: name, + Driver: driver, + DriverOpts: VolumeHelper.createDriverOptions(driverOptions), + }; + return volumeConfiguration; + }; + + service.createVolume = function (volumeConfiguration) { + var deferred = $q.defer(); + Volume.create(volumeConfiguration) + .$promise.then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message }); + } else { + var volume = new VolumeViewModel(data); + deferred.resolve(volume); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to create volume', err: err }); + }); + return deferred.promise; + }; + + service.createVolumes = function (volumeConfigurations) { + var createVolumeQueries = volumeConfigurations.map(function (volumeConfiguration) { + return service.createVolume(volumeConfiguration); + }); + return $q.all(createVolumeQueries); + }; + + service.createXAutoGeneratedLocalVolumes = function (x) { + var createVolumeQueries = []; + for (var i = 0; i < x; i++) { + createVolumeQueries.push(service.createVolume({ Driver: 'local' })); } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to create volume', err: err }); - }); - return deferred.promise; - }; + return $q.all(createVolumeQueries); + }; - service.createVolumes = function(volumeConfigurations) { - var createVolumeQueries = volumeConfigurations.map(function(volumeConfiguration) { - return service.createVolume(volumeConfiguration); - }); - return $q.all(createVolumeQueries); - }; - - service.createXAutoGeneratedLocalVolumes = function (x) { - var createVolumeQueries = []; - for (var i = 0; i < x; i++) { - createVolumeQueries.push(service.createVolume({ Driver: 'local' })); - } - return $q.all(createVolumeQueries); - }; - - return service; -}]); + return service; + }, +]); diff --git a/app/docker/views/configs/configs.html b/app/docker/views/configs/configs.html index d124e86c7..0145c737e 100644 --- a/app/docker/views/configs/configs.html +++ b/app/docker/views/configs/configs.html @@ -10,12 +10,14 @@
diff --git a/app/docker/views/configs/configsController.js b/app/docker/views/configs/configsController.js index 41cd2e313..013dbf9f7 100644 --- a/app/docker/views/configs/configsController.js +++ b/app/docker/views/configs/configsController.js @@ -1,7 +1,6 @@ import angular from 'angular'; class ConfigsController { - /* @ngInject */ constructor($state, ConfigService, Notifications, $async) { this.$state = $state; diff --git a/app/docker/views/configs/create/createConfigController.js b/app/docker/views/configs/create/createConfigController.js index 316250c47..5d1e686f9 100644 --- a/app/docker/views/configs/create/createConfigController.js +++ b/app/docker/views/configs/create/createConfigController.js @@ -1,7 +1,7 @@ -import _ from "lodash-es"; -import { AccessControlFormData } from "Portainer/components/accessControlForm/porAccessControlFormModel"; +import _ from 'lodash-es'; +import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel'; -import angular from "angular"; +import angular from 'angular'; class CreateConfigController { /* @ngInject */ @@ -16,14 +16,14 @@ class CreateConfigController { this.$async = $async; this.formValues = { - Name: "", + Name: '', Labels: [], AccessControlData: new AccessControlFormData(), - ConfigContent: "" + ConfigContent: '', }; this.state = { - formValidationError: "" + formValidationError: '', }; this.editorUpdate = this.editorUpdate.bind(this); @@ -38,7 +38,7 @@ class CreateConfigController { try { let data = await this.ConfigService.config(this.$transition$.params().id); - this.formValues.Name = data.Name + "_copy"; + this.formValues.Name = data.Name + '_copy'; this.formValues.Data = data.Data; let labels = _.keys(data.Labels); for (let i = 0; i < labels.length; i++) { @@ -49,12 +49,12 @@ class CreateConfigController { this.formValues.displayCodeEditor = true; } catch (err) { this.formValues.displayCodeEditor = true; - this.Notifications.error("Failure", err, "Unable to clone config"); + this.Notifications.error('Failure', err, 'Unable to clone config'); } } addLabel() { - this.formValues.Labels.push({ name: "", value: "" }); + this.formValues.Labels.push({ name: '', value: '' }); } removeLabel(index) { @@ -63,7 +63,7 @@ class CreateConfigController { prepareLabelsConfig(config) { let labels = {}; - this.formValues.Labels.forEach(function(label) { + this.formValues.Labels.forEach(function (label) { if (label.name && label.value) { labels[label.name] = label.value; } @@ -85,12 +85,9 @@ class CreateConfigController { } validateForm(accessControlData, isAdmin) { - this.state.formValidationError = ""; - let error = ""; - error = this.FormValidator.validateAccessControl( - accessControlData, - isAdmin - ); + this.state.formValidationError = ''; + let error = ''; + error = this.FormValidator.validateAccessControl(accessControlData, isAdmin); if (error) { this.state.formValidationError = error; @@ -108,8 +105,8 @@ class CreateConfigController { const userDetails = this.Authentication.getUserDetails(); const isAdmin = this.Authentication.isAdmin(); - if (this.formValues.ConfigContent === "") { - this.state.formValidationError = "Config content must not be empty"; + if (this.formValues.ConfigContent === '') { + this.state.formValidationError = 'Config content must not be empty'; return; } @@ -124,10 +121,10 @@ class CreateConfigController { const resourceControl = data.Portainer.ResourceControl; const userId = userDetails.ID; await this.ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); - this.Notifications.success("Config successfully created"); - this.$state.go("docker.configs", {}, { reload: true }); + this.Notifications.success('Config successfully created'); + this.$state.go('docker.configs', {}, { reload: true }); } catch (err) { - this.Notifications.error("Failure", err, "Unable to create config"); + this.Notifications.error('Failure', err, 'Unable to create config'); } } @@ -137,6 +134,4 @@ class CreateConfigController { } export default CreateConfigController; -angular - .module("portainer.docker") - .controller("CreateConfigController", CreateConfigController); +angular.module('portainer.docker').controller('CreateConfigController', CreateConfigController); diff --git a/app/docker/views/configs/create/createconfig.html b/app/docker/views/configs/create/createconfig.html index dbbfeda36..e92186639 100644 --- a/app/docker/views/configs/create/createconfig.html +++ b/app/docker/views/configs/create/createconfig.html @@ -1,8 +1,6 @@ - - Configs > Add config - + Configs > Add config
@@ -14,7 +12,7 @@
- +
@@ -25,7 +23,7 @@ identifier="config-creation-editor" placeholder="Define or paste the content of your config here" yml="false" - on-change="ctrl.editorUpdate" + on-change="(ctrl.editorUpdate)" value="ctrl.formValues.Data" >
@@ -44,11 +42,11 @@
name - +
value - +
- + + @@ -55,11 +59,7 @@
- + @@ -71,12 +71,7 @@
- +
diff --git a/app/docker/views/configs/edit/configController.js b/app/docker/views/configs/edit/configController.js index 2457c9609..516d65b69 100644 --- a/app/docker/views/configs/edit/configController.js +++ b/app/docker/views/configs/edit/configController.js @@ -1,27 +1,31 @@ -angular.module('portainer.docker') -.controller('ConfigController', ['$scope', '$transition$', '$state', 'ConfigService', 'Notifications', -function ($scope, $transition$, $state, ConfigService, Notifications) { +angular.module('portainer.docker').controller('ConfigController', [ + '$scope', + '$transition$', + '$state', + 'ConfigService', + 'Notifications', + function ($scope, $transition$, $state, ConfigService, Notifications) { + $scope.removeConfig = function removeConfig(configId) { + ConfigService.remove(configId) + .then(function success() { + Notifications.success('Config successfully removed'); + $state.go('docker.configs', {}); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove config'); + }); + }; - $scope.removeConfig = function removeConfig(configId) { - ConfigService.remove(configId) - .then(function success() { - Notifications.success('Config successfully removed'); - $state.go('docker.configs', {}); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove config'); - }); - }; + function initView() { + ConfigService.config($transition$.params().id) + .then(function success(data) { + $scope.config = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve config details'); + }); + } - function initView() { - ConfigService.config($transition$.params().id) - .then(function success(data) { - $scope.config = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve config details'); - }); - } - - initView(); -}]); + initView(); + }, +]); diff --git a/app/docker/views/containers/console/attach.html b/app/docker/views/containers/console/attach.html index 98e618848..703b57cb5 100644 --- a/app/docker/views/containers/console/attach.html +++ b/app/docker/views/containers/console/attach.html @@ -1,7 +1,7 @@ - Containers > {{ container.Name|trimcontainername }} > Console + Containers > {{ container.Name | trimcontainername }} > Console @@ -10,7 +10,6 @@ -

@@ -32,12 +31,16 @@

- -
diff --git a/app/docker/views/containers/console/containerConsoleController.js b/app/docker/views/containers/console/containerConsoleController.js index 2fa3c3e46..27bee1fa6 100644 --- a/app/docker/views/containers/console/containerConsoleController.js +++ b/app/docker/views/containers/console/containerConsoleController.js @@ -1,41 +1,63 @@ -import {Terminal} from 'xterm'; +import { Terminal } from 'xterm'; -angular.module('portainer.docker') - .controller('ContainerConsoleController', ['$scope', '$transition$', 'ContainerService', 'ImageService', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ExecService', 'HttpRequestHelper', 'LocalStorage', 'CONSOLE_COMMANDS_LABEL_PREFIX', - function ($scope, $transition$, ContainerService, ImageService, EndpointProvider, Notifications, ContainerHelper, ExecService, HttpRequestHelper, LocalStorage, CONSOLE_COMMANDS_LABEL_PREFIX) { - var socket, term; +angular.module('portainer.docker').controller('ContainerConsoleController', [ + '$scope', + '$transition$', + 'ContainerService', + 'ImageService', + 'EndpointProvider', + 'Notifications', + 'ContainerHelper', + 'ExecService', + 'HttpRequestHelper', + 'LocalStorage', + 'CONSOLE_COMMANDS_LABEL_PREFIX', + function ( + $scope, + $transition$, + ContainerService, + ImageService, + EndpointProvider, + Notifications, + ContainerHelper, + ExecService, + HttpRequestHelper, + LocalStorage, + CONSOLE_COMMANDS_LABEL_PREFIX + ) { + var socket, term; - let states = Object.freeze({ - disconnected: 0, - connecting: 1, - connected: 2, - }); + let states = Object.freeze({ + disconnected: 0, + connecting: 1, + connected: 2, + }); - $scope.loaded = false; - $scope.states = states; - $scope.state = states.disconnected; + $scope.loaded = false; + $scope.states = states; + $scope.state = states.disconnected; - $scope.formValues = {}; - $scope.containerCommands = []; + $scope.formValues = {}; + $scope.containerCommands = []; - // Ensure the socket is closed before leaving the view - $scope.$on('$stateChangeStart', function () { - $scope.disconnect(); - }); + // Ensure the socket is closed before leaving the view + $scope.$on('$stateChangeStart', function () { + $scope.disconnect(); + }); - $scope.connectAttach = function() { - if ($scope.state > states.disconnected) { - return; - } + $scope.connectAttach = function () { + if ($scope.state > states.disconnected) { + return; + } - $scope.state = states.connecting; + $scope.state = states.connecting; - let attachId = $transition$.params().id; - - ContainerService.container(attachId).then((details) => { + let attachId = $transition$.params().id; + ContainerService.container(attachId) + .then((details) => { if (!details.State.Running) { - Notifications.error("Failure", details, "Container " + attachId + " is not running!"); + Notifications.error('Failure', details, 'Container ' + attachId + ' is not running!'); $scope.disconnect(); return; } @@ -43,173 +65,177 @@ angular.module('portainer.docker') const params = { token: LocalStorage.getJWT(), endpointId: EndpointProvider.endpointID(), - id: attachId + id: attachId, }; - var url = window.location.href.split('#')[0] + 'api/websocket/attach?' + (Object.keys(params).map((k) => k + "=" + params[k]).join("&")); + var url = + window.location.href.split('#')[0] + + 'api/websocket/attach?' + + Object.keys(params) + .map((k) => k + '=' + params[k]) + .join('&'); initTerm(url, ContainerService.resizeTTY.bind(this, attachId)); }) - .catch(function error(err) { - Notifications.error('Error', err, 'Unable to retrieve container details'); - $scope.disconnect(); - }); - }; - - $scope.connectExec = function () { - if ($scope.state > states.disconnected) { - return; - } - - $scope.state = states.connecting; - var command = $scope.formValues.isCustomCommand ? - $scope.formValues.customCommand : $scope.formValues.command; - var execConfig = { - id: $transition$.params().id, - AttachStdin: true, - AttachStdout: true, - AttachStderr: true, - Tty: true, - User: $scope.formValues.user, - Cmd: ContainerHelper.commandStringToArray(command) - }; - - ContainerService.createExec(execConfig) - .then(function success(data) { - - const params = { - token: LocalStorage.getJWT(), - endpointId: EndpointProvider.endpointID(), - id: data.Id - }; - - var url = window.location.href.split('#')[0] + 'api/websocket/exec?' + (Object.keys(params).map((k) => k + "=" + params[k]).join("&")); - - initTerm(url, ExecService.resizeTTY.bind(this, params.id)); - - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to exec into container'); - $scope.disconnect(); - }); - }; - - $scope.disconnect = function () { - if (socket) { - socket.close(); - } - if ($scope.state > states.disconnected) { - $scope.state = states.disconnected; - if (term) { - term.write("\n\r(connection closed)"); - term.dispose(); - } - } - }; - - $scope.autoconnectAttachView = function () { - return $scope.initView().then(function success() { - if ($scope.container.State.Running) { - $scope.connectAttach(); - } + .catch(function error(err) { + Notifications.error('Error', err, 'Unable to retrieve container details'); + $scope.disconnect(); }); - }; + }; - function resize(restcall, add) { - add = add || 0; - - term.fit(); - var termWidth = term.cols; - var termHeight = 30; - term.resize(termWidth, termHeight); - - - restcall(termWidth + add, termHeight + add, 1); + $scope.connectExec = function () { + if ($scope.state > states.disconnected) { + return; } - function initTerm(url, resizeRestCall) { + $scope.state = states.connecting; + var command = $scope.formValues.isCustomCommand ? $scope.formValues.customCommand : $scope.formValues.command; + var execConfig = { + id: $transition$.params().id, + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Tty: true, + User: $scope.formValues.user, + Cmd: ContainerHelper.commandStringToArray(command), + }; - let resizefun = resize.bind(this, resizeRestCall); + ContainerService.createExec(execConfig) + .then(function success(data) { + const params = { + token: LocalStorage.getJWT(), + endpointId: EndpointProvider.endpointID(), + id: data.Id, + }; - if ($transition$.params().nodeName) { - url += '&nodeName=' + $transition$.params().nodeName; + var url = + window.location.href.split('#')[0] + + 'api/websocket/exec?' + + Object.keys(params) + .map((k) => k + '=' + params[k]) + .join('&'); + + initTerm(url, ExecService.resizeTTY.bind(this, params.id)); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to exec into container'); + $scope.disconnect(); + }); + }; + + $scope.disconnect = function () { + if (socket) { + socket.close(); + } + if ($scope.state > states.disconnected) { + $scope.state = states.disconnected; + if (term) { + term.write('\n\r(connection closed)'); + term.dispose(); } - if (url.indexOf('https') > -1) { - url = url.replace('https://', 'wss://'); - } else { - url = url.replace('http://', 'ws://'); + } + }; + + $scope.autoconnectAttachView = function () { + return $scope.initView().then(function success() { + if ($scope.container.State.Running) { + $scope.connectAttach(); } + }); + }; - socket = new WebSocket(url); + function resize(restcall, add) { + add = add || 0; + term.fit(); + var termWidth = term.cols; + var termHeight = 30; + term.resize(termWidth, termHeight); - socket.onopen = function () { - $scope.state = states.connected; - term = new Terminal(); + restcall(termWidth + add, termHeight + add, 1); + } + function initTerm(url, resizeRestCall) { + let resizefun = resize.bind(this, resizeRestCall); - term.on('data', function (data) { - socket.send(data); - }); - var terminal_container = document.getElementById('terminal-container'); - term.open(terminal_container); - term.focus(); - term.setOption('cursorBlink', true); + if ($transition$.params().nodeName) { + url += '&nodeName=' + $transition$.params().nodeName; + } + if (url.indexOf('https') > -1) { + url = url.replace('https://', 'wss://'); + } else { + url = url.replace('http://', 'ws://'); + } - window.onresize = function () { - resizefun(); - $scope.$apply(); - }; + socket = new WebSocket(url); - $scope.$watch('toggle', function () { - setTimeout(resizefun, 400); - }); + socket.onopen = function () { + $scope.state = states.connected; + term = new Terminal(); - socket.onmessage = function (e) { - term.write(e.data); - }; - socket.onerror = function (err) { - $scope.disconnect(); - $scope.$apply(); - Notifications.error("Failure", err, "Connection error"); - }; - socket.onclose = function () { - $scope.disconnect(); - $scope.$apply(); - }; + term.on('data', function (data) { + socket.send(data); + }); + var terminal_container = document.getElementById('terminal-container'); + term.open(terminal_container); + term.focus(); + term.setOption('cursorBlink', true); - resizefun(1); + window.onresize = function () { + resizefun(); $scope.$apply(); }; - } - $scope.initView = function () { - HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); - return ContainerService.container($transition$.params().id) - .then(function success(data) { - var container = data; - $scope.container = container; - return ImageService.image(container.Image); - }) - .then(function success(data) { - var image = data; - var containerLabels = $scope.container.Config.Labels; - $scope.imageOS = image.Os; - $scope.formValues.command = image.Os === 'windows' ? 'powershell' : 'bash'; - $scope.containerCommands = Object.keys(containerLabels) - .filter(function (label) { - return label.indexOf(CONSOLE_COMMANDS_LABEL_PREFIX) === 0; - }) - .map(function (label) { - return { - title: label.replace(CONSOLE_COMMANDS_LABEL_PREFIX, ''), - command: containerLabels[label] - }; - }); - $scope.loaded = true; - }) - .catch(function error(err) { - Notifications.error('Error', err, 'Unable to retrieve container details'); - }); - } - }]); + $scope.$watch('toggle', function () { + setTimeout(resizefun, 400); + }); + + socket.onmessage = function (e) { + term.write(e.data); + }; + socket.onerror = function (err) { + $scope.disconnect(); + $scope.$apply(); + Notifications.error('Failure', err, 'Connection error'); + }; + socket.onclose = function () { + $scope.disconnect(); + $scope.$apply(); + }; + + resizefun(1); + $scope.$apply(); + }; + } + + $scope.initView = function () { + HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); + return ContainerService.container($transition$.params().id) + .then(function success(data) { + var container = data; + $scope.container = container; + return ImageService.image(container.Image); + }) + .then(function success(data) { + var image = data; + var containerLabels = $scope.container.Config.Labels; + $scope.imageOS = image.Os; + $scope.formValues.command = image.Os === 'windows' ? 'powershell' : 'bash'; + $scope.containerCommands = Object.keys(containerLabels) + .filter(function (label) { + return label.indexOf(CONSOLE_COMMANDS_LABEL_PREFIX) === 0; + }) + .map(function (label) { + return { + title: label.replace(CONSOLE_COMMANDS_LABEL_PREFIX, ''), + command: containerLabels[label], + }; + }); + $scope.loaded = true; + }) + .catch(function error(err) { + Notifications.error('Error', err, 'Unable to retrieve container details'); + }); + }; + }, +]); diff --git a/app/docker/views/containers/console/exec.html b/app/docker/views/containers/console/exec.html index 31594f1c6..13ddfb32c 100644 --- a/app/docker/views/containers/console/exec.html +++ b/app/docker/views/containers/console/exec.html @@ -1,7 +1,7 @@ - Containers > {{ container.Name|trimcontainername }} > Console + Containers > {{ container.Name | trimcontainername }} > Console @@ -13,32 +13,30 @@
-
- -
-
- - - - - -
- +
+ +
+
+ + + + +
+
+
- +
- +
@@ -62,7 +60,10 @@
- + @@ -182,7 +196,7 @@
- +
@@ -190,7 +204,7 @@
- +
@@ -198,11 +212,11 @@
- +
- +
@@ -212,13 +226,13 @@
@@ -226,13 +240,13 @@
@@ -255,7 +269,8 @@

- Logging driver that will override the default docker daemon driver. Select Default logging driver if you don't want to override it. Supported logging drivers can be found in the Docker documentation. + Logging driver that will override the default docker daemon driver. Select Default logging driver if you don't want to override it. Supported logging drivers + can be found in the Docker documentation.

@@ -265,9 +280,16 @@
- + add logging driver option
@@ -276,11 +298,11 @@
option - +
value - +
- -
@@ -314,7 +334,7 @@
container - +
@@ -338,14 +358,14 @@ volume
host - +
@@ -399,7 +419,7 @@
- +
@@ -407,7 +427,7 @@
- +
@@ -415,7 +435,7 @@
- +
@@ -423,7 +443,7 @@
- +
@@ -431,7 +451,7 @@
- +
@@ -439,7 +459,7 @@
- +
@@ -447,7 +467,7 @@
- +
@@ -464,7 +484,7 @@
value - +
@@ -592,8 +610,7 @@
-
@@ -609,16 +626,16 @@ add device
- +
host - +
container - +
- +

@@ -658,7 +675,7 @@

- +

@@ -687,7 +704,7 @@

- +
diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html index 11a927aaa..2d7e9fadb 100644 --- a/app/docker/views/containers/edit/container.html +++ b/app/docker/views/containers/edit/container.html @@ -1,277 +1,326 @@ - - + - Containers > {{ container.Name|trimcontainername }} + Containers > {{ container.Name | trimcontainername }} -
-
- - - -
- - - - - - - +
+
+ + + +
+ + + + + + + +
+
+ + Duplicate/Edit +
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID{{ container.Id }}
Name + {{ container.Name | trimcontainername }} + + +
+ + + +
+
IP address{{ container.NetworkSettings.IPAddress }}
Status + + + {{ container.State | getstatetext }} for {{ activityTime + }} with exit code {{ container.State.ExitCode }} +
Created{{ container.Created | getisodate }}
Start time{{ container.State.StartedAt | getisodate }}
Finished{{ container.State.FinishedAt | getisodate }}
+ +
+
+
+
+
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
Status + + {{ container.State.Health.Status }} +
Failure count{{ container.State.Health.FailingStreak }}
Last output{{ container.State.Health.Log[container.State.Health.Log.length - 1].Output }}
+
+
+
+
+ +
+
+ + + +
+ +
+
+ + You can create an image from this container, this allows you to backup important data or save helpful configurations. You'll be able to spin up another container + based on this image afterward. + +
-
- - Duplicate/Edit + + + + + +
+
+ Note: if you don't specify the tag in the image name, latest will be used. +
- - -
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID{{ container.Id }}
Name - {{ container.Name|trimcontainername }} - - - - - - - -
IP address{{ container.NetworkSettings.IPAddress }}
Status - - - {{ container.State|getstatetext }} for {{ activityTime }} with exit code {{ container.State.ExitCode }} -
Created{{ container.Created|getisodate }}
Start time{{ container.State.StartedAt|getisodate }}
Finished{{ container.State.FinishedAt|getisodate }}
- -
-
-
-
-
- - - - - - -
-
- - - - - - - - - - - - - - - - - - -
Status - - {{ container.State.Health.Status }} -
Failure count{{ container.State.Health.FailingStreak }}
Last output{{ container.State.Health.Log[container.State.Health.Log.length - 1].Output }}
-
-
-
-
- -
-
- - - -
- -
-
- - You can create an image from this container, this allows you to backup important data or save - helpful configurations. You'll be able to spin up another container based on this image afterward. - -
+ +
+
+
- - - - - -
-
- Note: if you don't specify the tag in the image name, latest will be used. -
-
- -
-
- -
-
- - - -
+
+ +
+
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Image{{ container.Config.Image}}@{{container.Image}}
Port configuration -
- {{ portMapping.host }} {{ portMapping.container }} -
-
CMD{{ container.Config.Cmd|command }}
ENTRYPOINT{{ container.Config.Entrypoint ? (container.Config.Entrypoint|command) : "null" }}
ENV - - - - - -
{{ var|key: '=' }}{{ var|value: '=' }}
-
Labels - - - - - -
{{ k }}{{ v }}
-
Restart policies - -
-
-
-
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Image{{ container.Config.Image }}@{{ container.Image }}
Port configuration +
{{ portMapping.host }} {{ portMapping.container }}
+
CMD{{ container.Config.Cmd | command }}
ENTRYPOINT{{ container.Config.Entrypoint ? (container.Config.Entrypoint | command) : 'null' }}
ENV + + + + + +
{{ var|key: '=' }}{{ var|value: '=' }}
+
Labels + + + + + +
{{ k }}{{ v }}
+
Restart policies + + +
+
+
+
-
-
- - - - - - - - - - - - - - - - - -
Host/volumePath in container
{{ vol.Source }}{{ vol.Name }}{{ vol.Destination }}
-
-
-
+
+
+ + + + + + + + + + + + + + + + + +
Host/volumePath in container
{{ vol.Source }}{{ vol.Name }}{{ vol.Destination }}
+
+
+
-
-
- +
+ -
+ >
+
diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index f775fbc1e..6528669f1 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -1,356 +1,383 @@ import moment from 'moment'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; -angular.module('portainer.docker') -.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', '$async', 'ExtensionService', 'Commit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', 'HttpRequestHelper', 'Authentication', -function ($q, $scope, $state, $transition$, $filter, $async, ExtensionService, Commit, ContainerHelper, ContainerService, ImageHelper, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService, HttpRequestHelper, Authentication) { - $scope.activityTime = 0; - $scope.portBindings = []; - $scope.displayRecreateButton = false; +angular.module('portainer.docker').controller('ContainerController', [ + '$q', + '$scope', + '$state', + '$transition$', + '$filter', + '$async', + 'ExtensionService', + 'Commit', + 'ContainerHelper', + 'ContainerService', + 'ImageHelper', + 'NetworkService', + 'Notifications', + 'ModalService', + 'ResourceControlService', + 'RegistryService', + 'ImageService', + 'HttpRequestHelper', + 'Authentication', + function ( + $q, + $scope, + $state, + $transition$, + $filter, + $async, + ExtensionService, + Commit, + ContainerHelper, + ContainerService, + ImageHelper, + NetworkService, + Notifications, + ModalService, + ResourceControlService, + RegistryService, + ImageService, + HttpRequestHelper, + Authentication + ) { + $scope.activityTime = 0; + $scope.portBindings = []; + $scope.displayRecreateButton = false; - $scope.config = { - RegistryModel: new PorImageRegistryModel(), - commitInProgress: false - }; + $scope.config = { + RegistryModel: new PorImageRegistryModel(), + commitInProgress: false, + }; - $scope.state = { - recreateContainerInProgress: false, - joinNetworkInProgress: false, - leaveNetworkInProgress: false - }; + $scope.state = { + recreateContainerInProgress: false, + joinNetworkInProgress: false, + leaveNetworkInProgress: false, + }; - $scope.updateRestartPolicy = updateRestartPolicy; + $scope.updateRestartPolicy = updateRestartPolicy; - var update = function () { - var nodeName = $transition$.params().nodeName; - HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); - $scope.nodeName = nodeName; + var update = function () { + var nodeName = $transition$.params().nodeName; + HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); + $scope.nodeName = nodeName; - ContainerService.container($transition$.params().id) - .then(function success(data) { - var container = data; - $scope.container = container; - $scope.container.edit = false; - $scope.container.newContainerName = $filter('trimcontainername')(container.Name); + ContainerService.container($transition$.params().id) + .then(function success(data) { + var container = data; + $scope.container = container; + $scope.container.edit = false; + $scope.container.newContainerName = $filter('trimcontainername')(container.Name); - if (container.State.Running) { - $scope.activityTime = moment.duration(moment(container.State.StartedAt).utc().diff(moment().utc())).humanize(); - } else if (container.State.Status === 'created') { - $scope.activityTime = moment.duration(moment(container.Created).utc().diff(moment().utc())).humanize(); - } else { - $scope.activityTime = moment.duration(moment().utc().diff(moment(container.State.FinishedAt).utc())).humanize(); - } - - $scope.portBindings = []; - if (container.NetworkSettings.Ports) { - angular.forEach(Object.keys(container.NetworkSettings.Ports), function(portMapping) { - if (container.NetworkSettings.Ports[portMapping]) { - var mapping = {}; - mapping.container = portMapping; - mapping.host = container.NetworkSettings.Ports[portMapping][0].HostIp + ':' + container.NetworkSettings.Ports[portMapping][0].HostPort; - $scope.portBindings.push(mapping); + if (container.State.Running) { + $scope.activityTime = moment.duration(moment(container.State.StartedAt).utc().diff(moment().utc())).humanize(); + } else if (container.State.Status === 'created') { + $scope.activityTime = moment.duration(moment(container.Created).utc().diff(moment().utc())).humanize(); + } else { + $scope.activityTime = moment.duration(moment().utc().diff(moment(container.State.FinishedAt).utc())).humanize(); } + + $scope.portBindings = []; + if (container.NetworkSettings.Ports) { + angular.forEach(Object.keys(container.NetworkSettings.Ports), function (portMapping) { + if (container.NetworkSettings.Ports[portMapping]) { + var mapping = {}; + mapping.container = portMapping; + mapping.host = container.NetworkSettings.Ports[portMapping][0].HostIp + ':' + container.NetworkSettings.Ports[portMapping][0].HostPort; + $scope.portBindings.push(mapping); + } + }); + } + + const inSwarm = $scope.container.Config.Labels['com.docker.swarm.service.id']; + const autoRemove = $scope.container.HostConfig.AutoRemove; + const admin = Authentication.isAdmin(); + + ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC).then((rbacEnabled) => { + $scope.displayRecreateButton = !inSwarm && !autoRemove && (rbacEnabled ? admin : true); + }); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve container info'); }); + }; + + function executeContainerAction(id, action, successMessage, errorMessage) { + action(id) + .then(function success() { + Notifications.success(successMessage, id); + update(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, errorMessage); + }); + } + + $scope.start = function () { + var successMessage = 'Container successfully started'; + var errorMessage = 'Unable to start container'; + executeContainerAction($transition$.params().id, ContainerService.startContainer, successMessage, errorMessage); + }; + + $scope.stop = function () { + var successMessage = 'Container successfully stopped'; + var errorMessage = 'Unable to stop container'; + executeContainerAction($transition$.params().id, ContainerService.stopContainer, successMessage, errorMessage); + }; + + $scope.kill = function () { + var successMessage = 'Container successfully killed'; + var errorMessage = 'Unable to kill container'; + executeContainerAction($transition$.params().id, ContainerService.killContainer, successMessage, errorMessage); + }; + + $scope.pause = function () { + var successMessage = 'Container successfully paused'; + var errorMessage = 'Unable to pause container'; + executeContainerAction($transition$.params().id, ContainerService.pauseContainer, successMessage, errorMessage); + }; + + $scope.unpause = function () { + var successMessage = 'Container successfully resumed'; + var errorMessage = 'Unable to resume container'; + executeContainerAction($transition$.params().id, ContainerService.resumeContainer, successMessage, errorMessage); + }; + + $scope.restart = function () { + var successMessage = 'Container successfully restarted'; + var errorMessage = 'Unable to restart container'; + executeContainerAction($transition$.params().id, ContainerService.restartContainer, successMessage, errorMessage); + }; + + $scope.renameContainer = function () { + var container = $scope.container; + ContainerService.renameContainer($transition$.params().id, container.newContainerName) + .then(function success() { + container.Name = container.newContainerName; + Notifications.success('Container successfully renamed', container.Name); + }) + .catch(function error(err) { + container.newContainerName = container.Name; + Notifications.error('Failure', err, 'Unable to rename container'); + }) + .finally(function final() { + $scope.container.edit = false; + }); + }; + + $scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) { + $scope.state.leaveNetworkInProgress = true; + NetworkService.disconnectContainer(networkId, container.Id, false) + .then(function success() { + Notifications.success('Container left network', container.Id); + $state.reload(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to disconnect container from network'); + }) + .finally(function final() { + $scope.state.leaveNetworkInProgress = false; + }); + }; + + $scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) { + $scope.state.joinNetworkInProgress = true; + NetworkService.connectContainer(networkId, container.Id) + .then(function success() { + Notifications.success('Container joined network', container.Id); + $state.reload(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to connect container to network'); + }) + .finally(function final() { + $scope.state.joinNetworkInProgress = false; + }); + }; + + async function commitContainerAsync() { + $scope.config.commitInProgress = true; + const registryModel = $scope.config.RegistryModel; + const imageConfig = ImageHelper.createImageConfigForContainer(registryModel); + try { + await Commit.commitContainer({ id: $transition$.params().id, repo: imageConfig.fromImage }).$promise; + Notifications.success('Image created', $transition$.params().id); + $state.reload(); + } catch (err) { + Notifications.error('Failure', err, 'Unable to create image'); + $scope.config.commitInProgress = false; } - - const inSwarm = $scope.container.Config.Labels['com.docker.swarm.service.id']; - const autoRemove = $scope.container.HostConfig.AutoRemove; - const admin = Authentication.isAdmin(); - - ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC).then((rbacEnabled) => { - $scope.displayRecreateButton = !inSwarm && !autoRemove && (rbacEnabled ? admin : true) - }); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve container info'); - }); - }; - - function executeContainerAction(id, action, successMessage, errorMessage) { - action(id) - .then(function success() { - Notifications.success(successMessage, id); - update(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, errorMessage); - }); - } - - $scope.start = function () { - var successMessage = 'Container successfully started'; - var errorMessage = 'Unable to start container'; - executeContainerAction($transition$.params().id, ContainerService.startContainer, successMessage, errorMessage); - }; - - $scope.stop = function () { - var successMessage = 'Container successfully stopped'; - var errorMessage = 'Unable to stop container'; - executeContainerAction($transition$.params().id, ContainerService.stopContainer, successMessage, errorMessage); - }; - - $scope.kill = function () { - var successMessage = 'Container successfully killed'; - var errorMessage = 'Unable to kill container'; - executeContainerAction($transition$.params().id, ContainerService.killContainer, successMessage, errorMessage); - }; - - $scope.pause = function() { - var successMessage = 'Container successfully paused'; - var errorMessage = 'Unable to pause container'; - executeContainerAction($transition$.params().id, ContainerService.pauseContainer, successMessage, errorMessage); - }; - - $scope.unpause = function() { - var successMessage = 'Container successfully resumed'; - var errorMessage = 'Unable to resume container'; - executeContainerAction($transition$.params().id, ContainerService.resumeContainer, successMessage, errorMessage); - }; - - $scope.restart = function () { - var successMessage = 'Container successfully restarted'; - var errorMessage = 'Unable to restart container'; - executeContainerAction($transition$.params().id, ContainerService.restartContainer, successMessage, errorMessage); - }; - - $scope.renameContainer = function () { - var container = $scope.container; - ContainerService.renameContainer($transition$.params().id, container.newContainerName) - .then(function success() { - container.Name = container.newContainerName; - Notifications.success('Container successfully renamed', container.Name); - }) - .catch(function error(err) { - container.newContainerName = container.Name; - Notifications.error('Failure', err, 'Unable to rename container'); - }) - .finally(function final() { - $scope.container.edit = false; - }); - }; - - $scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) { - $scope.state.leaveNetworkInProgress = true; - NetworkService.disconnectContainer(networkId, container.Id, false) - .then(function success() { - Notifications.success('Container left network', container.Id); - $state.reload(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to disconnect container from network'); - }) - .finally(function final() { - $scope.state.leaveNetworkInProgress = false; - }); - }; - - $scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) { - $scope.state.joinNetworkInProgress = true; - NetworkService.connectContainer(networkId, container.Id) - .then(function success() { - Notifications.success('Container joined network', container.Id); - $state.reload(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect container to network'); - }) - .finally(function final() { - $scope.state.joinNetworkInProgress = false; - }); - }; - - async function commitContainerAsync() { - $scope.config.commitInProgress = true; - const registryModel = $scope.config.RegistryModel; - const imageConfig = ImageHelper.createImageConfigForContainer(registryModel); - try { - await Commit.commitContainer({id: $transition$.params().id, repo: imageConfig.fromImage}).$promise; - Notifications.success('Image created', $transition$.params().id); - $state.reload(); - } catch (err) { - Notifications.error('Failure', err, 'Unable to create image'); - $scope.config.commitInProgress = false; } - } - $scope.commit = function () { - return $async(commitContainerAsync); - }; + $scope.commit = function () { + return $async(commitContainerAsync); + }; - $scope.confirmRemove = function () { - var title = 'You are about to remove a container.'; - if ($scope.container.State.Running) { - title = 'You are about to remove a running container.'; - } - ModalService.confirmContainerDeletion( - title, - function (result) { - if(!result) { return; } + $scope.confirmRemove = function () { + var title = 'You are about to remove a container.'; + if ($scope.container.State.Running) { + title = 'You are about to remove a running container.'; + } + ModalService.confirmContainerDeletion(title, function (result) { + if (!result) { + return; + } var cleanAssociatedVolumes = false; if (result[0]) { cleanAssociatedVolumes = true; } removeContainer(cleanAssociatedVolumes); - } - ); - }; - - function removeContainer(cleanAssociatedVolumes) { - ContainerService.remove($scope.container, cleanAssociatedVolumes) - .then(function success() { - Notifications.success('Container successfully removed'); - $state.go('docker.containers', {}, {reload: true}); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove container'); - }); - } - - function recreateContainer(pullImage) { - var container = $scope.container; - var config = ContainerHelper.configFromContainer(container.Model); - $scope.state.recreateContainerInProgress = true; - var isRunning = container.State.Running; - - return pullImageIfNeeded() - .then(stopContainerIfNeeded) - .then(renameContainer) - .then(setMainNetworkAndCreateContainer) - .then(connectContainerToOtherNetworks) - .then(startContainerIfNeeded) - .then(createResourceControl) - .then(deleteOldContainer) - .then(notifyAndChangeView) - .catch(notifyOnError); - - function stopContainerIfNeeded() { - if (!isRunning) { - return $q.when(); - } - return ContainerService.stopContainer(container.Id); - } - - function renameContainer() { - return ContainerService.renameContainer(container.Id, container.Name + '-old'); - } - - function pullImageIfNeeded() { - if (!pullImage) { - return $q.when(); - } - return RegistryService.retrievePorRegistryModelFromRepository(container.Config.Image) - .then(function pullImage(registryModel) { - return ImageService.pullImage(registryModel, true); }); - } + }; - function setMainNetworkAndCreateContainer() { - var networks = config.NetworkingConfig.EndpointsConfig; - var networksNames = Object.keys(networks); - if (networksNames.length > 1) { - config.NetworkingConfig.EndpointsConfig = {}; - config.NetworkingConfig.EndpointsConfig[networksNames[0]] = networks[0]; - } - return $q.all([ContainerService.createContainer(config), networks]); - } - - function connectContainerToOtherNetworks(createContainerData) { - var newContainer = createContainerData[0]; - var networks = createContainerData[1]; - var networksNames = Object.keys(networks); - var connectionPromises = networksNames.map(function connectToNetwork(name) { - NetworkService.connectContainer(name, newContainer.Id); - }); - return $q.all(connectionPromises) - .then(function onConnectToNetworkSuccess() { - return newContainer; + function removeContainer(cleanAssociatedVolumes) { + ContainerService.remove($scope.container, cleanAssociatedVolumes) + .then(function success() { + Notifications.success('Container successfully removed'); + $state.go('docker.containers', {}, { reload: true }); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove container'); }); } - function deleteOldContainer(newContainer) { - return ContainerService.remove(container, true).then( - function onRemoveSuccess() { - return newContainer; + function recreateContainer(pullImage) { + var container = $scope.container; + var config = ContainerHelper.configFromContainer(container.Model); + $scope.state.recreateContainerInProgress = true; + var isRunning = container.State.Running; + + return pullImageIfNeeded() + .then(stopContainerIfNeeded) + .then(renameContainer) + .then(setMainNetworkAndCreateContainer) + .then(connectContainerToOtherNetworks) + .then(startContainerIfNeeded) + .then(createResourceControl) + .then(deleteOldContainer) + .then(notifyAndChangeView) + .catch(notifyOnError); + + function stopContainerIfNeeded() { + if (!isRunning) { + return $q.when(); } - ); - } - - function startContainerIfNeeded(newContainer) { - if (!isRunning) { - return $q.when(newContainer); + return ContainerService.stopContainer(container.Id); } - return ContainerService.startContainer(newContainer.Id).then( - function onStartSuccess() { - return newContainer; + + function renameContainer() { + return ContainerService.renameContainer(container.Id, container.Name + '-old'); + } + + function pullImageIfNeeded() { + if (!pullImage) { + return $q.when(); } - ); - } - - function createResourceControl(newContainer) { - const userId = Authentication.getUserDetails().ID; - const oldResourceControl = container.ResourceControl; - const newResourceControl = newContainer.Portainer.ResourceControl; - return ResourceControlService.duplicateResourceControl(userId, oldResourceControl, newResourceControl); - } - - function notifyAndChangeView() { - Notifications.success('Container successfully re-created'); - $state.go('docker.containers', {}, { reload: true }); - } - - function notifyOnError(err) { - Notifications.error('Failure', err, 'Unable to re-create container'); - $scope.state.recreateContainerInProgress = false; - } - } - - $scope.recreate = function() { - ModalService.confirmContainerRecreation(function (result) { - if(!result) { return; } - var pullImage = false; - if (result[0]) { - pullImage = true; + return RegistryService.retrievePorRegistryModelFromRepository(container.Config.Image).then(function pullImage(registryModel) { + return ImageService.pullImage(registryModel, true); + }); } - recreateContainer(pullImage); - }); - }; - function updateRestartPolicy(restartPolicy, maximumRetryCount) { - maximumRetryCount = restartPolicy === 'on-failure' ? maximumRetryCount : undefined; + function setMainNetworkAndCreateContainer() { + var networks = config.NetworkingConfig.EndpointsConfig; + var networksNames = Object.keys(networks); + if (networksNames.length > 1) { + config.NetworkingConfig.EndpointsConfig = {}; + config.NetworkingConfig.EndpointsConfig[networksNames[0]] = networks[0]; + } + return $q.all([ContainerService.createContainer(config), networks]); + } - return ContainerService - .updateRestartPolicy($scope.container.Id, restartPolicy, maximumRetryCount) - .then(onUpdateSuccess) - .catch(notifyOnError); + function connectContainerToOtherNetworks(createContainerData) { + var newContainer = createContainerData[0]; + var networks = createContainerData[1]; + var networksNames = Object.keys(networks); + var connectionPromises = networksNames.map(function connectToNetwork(name) { + NetworkService.connectContainer(name, newContainer.Id); + }); + return $q.all(connectionPromises).then(function onConnectToNetworkSuccess() { + return newContainer; + }); + } - function onUpdateSuccess() { - $scope.container.HostConfig.RestartPolicy = { - Name: restartPolicy, - MaximumRetryCount: maximumRetryCount - }; - Notifications.success('Restart policy updated'); + function deleteOldContainer(newContainer) { + return ContainerService.remove(container, true).then(function onRemoveSuccess() { + return newContainer; + }); + } + + function startContainerIfNeeded(newContainer) { + if (!isRunning) { + return $q.when(newContainer); + } + return ContainerService.startContainer(newContainer.Id).then(function onStartSuccess() { + return newContainer; + }); + } + + function createResourceControl(newContainer) { + const userId = Authentication.getUserDetails().ID; + const oldResourceControl = container.ResourceControl; + const newResourceControl = newContainer.Portainer.ResourceControl; + return ResourceControlService.duplicateResourceControl(userId, oldResourceControl, newResourceControl); + } + + function notifyAndChangeView() { + Notifications.success('Container successfully re-created'); + $state.go('docker.containers', {}, { reload: true }); + } + + function notifyOnError(err) { + Notifications.error('Failure', err, 'Unable to re-create container'); + $scope.state.recreateContainerInProgress = false; + } } - function notifyOnError(err) { - Notifications.error('Failure', err, 'Unable to update restart policy'); - return $q.reject(err); + $scope.recreate = function () { + ModalService.confirmContainerRecreation(function (result) { + if (!result) { + return; + } + var pullImage = false; + if (result[0]) { + pullImage = true; + } + recreateContainer(pullImage); + }); + }; + + function updateRestartPolicy(restartPolicy, maximumRetryCount) { + maximumRetryCount = restartPolicy === 'on-failure' ? maximumRetryCount : undefined; + + return ContainerService.updateRestartPolicy($scope.container.Id, restartPolicy, maximumRetryCount).then(onUpdateSuccess).catch(notifyOnError); + + function onUpdateSuccess() { + $scope.container.HostConfig.RestartPolicy = { + Name: restartPolicy, + MaximumRetryCount: maximumRetryCount, + }; + Notifications.success('Restart policy updated'); + } + + function notifyOnError(err) { + Notifications.error('Failure', err, 'Unable to update restart policy'); + return $q.reject(err); + } } - } - var provider = $scope.applicationState.endpoint.mode.provider; - var apiVersion = $scope.applicationState.endpoint.apiVersion; - NetworkService.networks( - provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', - false, - provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25 - ) - .then(function success(data) { - var networks = data; - $scope.availableNetworks = networks; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve networks'); - }); + var provider = $scope.applicationState.endpoint.mode.provider; + var apiVersion = $scope.applicationState.endpoint.apiVersion; + NetworkService.networks(provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', false, provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25) + .then(function success(data) { + var networks = data; + $scope.availableNetworks = networks; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve networks'); + }); - update(); -}]); + update(); + }, +]); diff --git a/app/docker/views/containers/inspect/containerInspectController.js b/app/docker/views/containers/inspect/containerInspectController.js index 458a0e6a4..823f9d2b6 100644 --- a/app/docker/views/containers/inspect/containerInspectController.js +++ b/app/docker/views/containers/inspect/containerInspectController.js @@ -1,22 +1,26 @@ -angular.module('portainer.docker') -.controller('ContainerInspectController', ['$scope', '$transition$', 'Notifications', 'ContainerService', 'HttpRequestHelper', -function ($scope, $transition$, Notifications, ContainerService, HttpRequestHelper) { +angular.module('portainer.docker').controller('ContainerInspectController', [ + '$scope', + '$transition$', + 'Notifications', + 'ContainerService', + 'HttpRequestHelper', + function ($scope, $transition$, Notifications, ContainerService, HttpRequestHelper) { + $scope.state = { + DisplayTextView: false, + }; + $scope.containerInfo = {}; - $scope.state = { - DisplayTextView: false - }; - $scope.containerInfo = {}; + function initView() { + HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); + ContainerService.inspect($transition$.params().id) + .then(function success(d) { + $scope.containerInfo = d; + }) + .catch(function error(e) { + Notifications.error('Failure', e, 'Unable to inspect container'); + }); + } - function initView() { - HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); - ContainerService.inspect($transition$.params().id) - .then(function success(d) { - $scope.containerInfo = d; - }) - .catch(function error(e) { - Notifications.error('Failure', e, 'Unable to inspect container'); - }); - } - - initView(); -}]); + initView(); + }, +]); diff --git a/app/docker/views/containers/inspect/containerinspect.html b/app/docker/views/containers/inspect/containerinspect.html index b381ed2bb..def4954d5 100644 --- a/app/docker/views/containers/inspect/containerinspect.html +++ b/app/docker/views/containers/inspect/containerinspect.html @@ -1,8 +1,8 @@ - - + - Containers > {{ containerInfo.Name|trimcontainername }} > Inspect + Containers > {{ containerInfo.Name | trimcontainername }} > + Inspect @@ -16,7 +16,7 @@ -
{{ containerInfo|json:4 }}
+
{{ containerInfo | json: 4 }}
diff --git a/app/docker/views/containers/logs/containerLogsController.js b/app/docker/views/containers/logs/containerLogsController.js index 695bfd339..d6799a451 100644 --- a/app/docker/views/containers/logs/containerLogsController.js +++ b/app/docker/views/containers/logs/containerLogsController.js @@ -1,73 +1,87 @@ import moment from 'moment'; -angular.module('portainer.docker') -.controller('ContainerLogsController', ['$scope', '$transition$', '$interval', 'ContainerService', 'Notifications', 'HttpRequestHelper', -function ($scope, $transition$, $interval, ContainerService, Notifications, HttpRequestHelper) { - $scope.state = { - refreshRate: 3, - lineCount: 100, - sinceTimestamp: '', - displayTimestamps: false - }; +angular.module('portainer.docker').controller('ContainerLogsController', [ + '$scope', + '$transition$', + '$interval', + 'ContainerService', + 'Notifications', + 'HttpRequestHelper', + function ($scope, $transition$, $interval, ContainerService, Notifications, HttpRequestHelper) { + $scope.state = { + refreshRate: 3, + lineCount: 100, + sinceTimestamp: '', + displayTimestamps: false, + }; - $scope.changeLogCollection = function(logCollectionStatus) { - if (!logCollectionStatus) { - stopRepeater(); - } else { - setUpdateRepeater(!$scope.container.Config.Tty); - } - }; - - $scope.$on('$destroy', function() { - stopRepeater(); - }); - - function stopRepeater() { - var repeater = $scope.repeater; - if (angular.isDefined(repeater)) { - $interval.cancel(repeater); - repeater = null; - } - } - - function setUpdateRepeater(skipHeaders) { - var refreshRate = $scope.state.refreshRate; - $scope.repeater = $interval(function() { - ContainerService.logs($transition$.params().id, 1, 1, $scope.state.displayTimestamps ? 1 : 0, moment($scope.state.sinceTimestamp).unix(), $scope.state.lineCount, skipHeaders) - .then(function success(data) { - $scope.logs = data; - }) - .catch(function error(err) { + $scope.changeLogCollection = function (logCollectionStatus) { + if (!logCollectionStatus) { stopRepeater(); - Notifications.error('Failure', err, 'Unable to retrieve container logs'); - }); - }, refreshRate * 1000); - } + } else { + setUpdateRepeater(!$scope.container.Config.Tty); + } + }; - function startLogPolling(skipHeaders) { - ContainerService.logs($transition$.params().id, 1, 1, $scope.state.displayTimestamps ? 1 : 0, moment($scope.state.sinceTimestamp).unix(), $scope.state.lineCount, skipHeaders) - .then(function success(data) { - $scope.logs = data; - setUpdateRepeater(skipHeaders); - }) - .catch(function error(err) { + $scope.$on('$destroy', function () { stopRepeater(); - Notifications.error('Failure', err, 'Unable to retrieve container logs'); }); - } - function initView() { - HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); - ContainerService.container($transition$.params().id) - .then(function success(data) { - var container = data; - $scope.container = container; - startLogPolling(!container.Config.Tty); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve container information'); - }); - } + function stopRepeater() { + var repeater = $scope.repeater; + if (angular.isDefined(repeater)) { + $interval.cancel(repeater); + repeater = null; + } + } - initView(); -}]); + function setUpdateRepeater(skipHeaders) { + var refreshRate = $scope.state.refreshRate; + $scope.repeater = $interval(function () { + ContainerService.logs( + $transition$.params().id, + 1, + 1, + $scope.state.displayTimestamps ? 1 : 0, + moment($scope.state.sinceTimestamp).unix(), + $scope.state.lineCount, + skipHeaders + ) + .then(function success(data) { + $scope.logs = data; + }) + .catch(function error(err) { + stopRepeater(); + Notifications.error('Failure', err, 'Unable to retrieve container logs'); + }); + }, refreshRate * 1000); + } + + function startLogPolling(skipHeaders) { + ContainerService.logs($transition$.params().id, 1, 1, $scope.state.displayTimestamps ? 1 : 0, moment($scope.state.sinceTimestamp).unix(), $scope.state.lineCount, skipHeaders) + .then(function success(data) { + $scope.logs = data; + setUpdateRepeater(skipHeaders); + }) + .catch(function error(err) { + stopRepeater(); + Notifications.error('Failure', err, 'Unable to retrieve container logs'); + }); + } + + function initView() { + HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); + ContainerService.container($transition$.params().id) + .then(function success(data) { + var container = data; + $scope.container = container; + startLogPolling(!container.Config.Tty); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve container information'); + }); + } + + initView(); + }, +]); diff --git a/app/docker/views/containers/logs/containerlogs.html b/app/docker/views/containers/logs/containerlogs.html index 133e354c2..0cba93e8a 100644 --- a/app/docker/views/containers/logs/containerlogs.html +++ b/app/docker/views/containers/logs/containerlogs.html @@ -1,10 +1,15 @@ - Containers > {{ container.Name|trimcontainername }} > Logs + Containers > {{ container.Name | trimcontainername }} > Logs diff --git a/app/docker/views/containers/stats/containerStatsController.js b/app/docker/views/containers/stats/containerStatsController.js index a1c57c10c..9fef44e41 100644 --- a/app/docker/views/containers/stats/containerStatsController.js +++ b/app/docker/views/containers/stats/containerStatsController.js @@ -1,156 +1,163 @@ import moment from 'moment'; -angular.module('portainer.docker') -.controller('ContainerStatsController', ['$q', '$scope', '$transition$', '$document', '$interval', 'ContainerService', 'ChartService', 'Notifications', 'HttpRequestHelper', -function ($q, $scope, $transition$, $document, $interval, ContainerService, ChartService, Notifications, HttpRequestHelper) { +angular.module('portainer.docker').controller('ContainerStatsController', [ + '$q', + '$scope', + '$transition$', + '$document', + '$interval', + 'ContainerService', + 'ChartService', + 'Notifications', + 'HttpRequestHelper', + function ($q, $scope, $transition$, $document, $interval, ContainerService, ChartService, Notifications, HttpRequestHelper) { + $scope.state = { + refreshRate: '5', + networkStatsUnavailable: false, + }; - $scope.state = { - refreshRate: '5', - networkStatsUnavailable: false - }; + $scope.$on('$destroy', function () { + stopRepeater(); + }); - $scope.$on('$destroy', function() { - stopRepeater(); - }); - - function stopRepeater() { - var repeater = $scope.repeater; - if (angular.isDefined(repeater)) { - $interval.cancel(repeater); - repeater = null; + function stopRepeater() { + var repeater = $scope.repeater; + if (angular.isDefined(repeater)) { + $interval.cancel(repeater); + repeater = null; + } } - } - function updateNetworkChart(stats, chart) { - if (stats.Networks.length > 0) { - var rx = stats.Networks[0].rx_bytes; - var tx = stats.Networks[0].tx_bytes; + function updateNetworkChart(stats, chart) { + if (stats.Networks.length > 0) { + var rx = stats.Networks[0].rx_bytes; + var tx = stats.Networks[0].tx_bytes; + var label = moment(stats.read).format('HH:mm:ss'); + + ChartService.UpdateNetworkChart(label, rx, tx, chart); + } + } + + function updateMemoryChart(stats, chart) { var label = moment(stats.read).format('HH:mm:ss'); - ChartService.UpdateNetworkChart(label, rx, tx, chart); - } - } - - function updateMemoryChart(stats, chart) { - var label = moment(stats.read).format('HH:mm:ss'); - - ChartService.UpdateMemoryChart(label, stats.MemoryUsage, stats.MemoryCache, chart); - } - - function updateCPUChart(stats, chart) { - var label = moment(stats.read).format('HH:mm:ss'); - var value = stats.isWindows ? calculateCPUPercentWindows(stats) : calculateCPUPercentUnix(stats); - - ChartService.UpdateCPUChart(label, value, chart); - } - - function calculateCPUPercentUnix(stats) { - var cpuPercent = 0.0; - var cpuDelta = stats.CurrentCPUTotalUsage - stats.PreviousCPUTotalUsage; - var systemDelta = stats.CurrentCPUSystemUsage - stats.PreviousCPUSystemUsage; - - if (systemDelta > 0.0 && cpuDelta > 0.0) { - cpuPercent = (cpuDelta / systemDelta) * stats.CPUCores * 100.0; + ChartService.UpdateMemoryChart(label, stats.MemoryUsage, stats.MemoryCache, chart); } - return cpuPercent; - } + function updateCPUChart(stats, chart) { + var label = moment(stats.read).format('HH:mm:ss'); + var value = stats.isWindows ? calculateCPUPercentWindows(stats) : calculateCPUPercentUnix(stats); - function calculateCPUPercentWindows(stats) { - var possIntervals = stats.NumProcs * parseFloat( - moment(stats.read, 'YYYY-MM-DDTHH:mm:ss.SSSSSSSSSZ').valueOf() - moment(stats.preread, 'YYYY-MM-DDTHH:mm:ss.SSSSSSSSSZ').valueOf()); - var windowsCpuUsage = 0.0; - if(possIntervals > 0) { - windowsCpuUsage = parseFloat(stats.CurrentCPUTotalUsage - stats.PreviousCPUTotalUsage) / parseFloat(possIntervals * 100); + ChartService.UpdateCPUChart(label, value, chart); } - return windowsCpuUsage; - } + function calculateCPUPercentUnix(stats) { + var cpuPercent = 0.0; + var cpuDelta = stats.CurrentCPUTotalUsage - stats.PreviousCPUTotalUsage; + var systemDelta = stats.CurrentCPUSystemUsage - stats.PreviousCPUSystemUsage; - $scope.changeUpdateRepeater = function() { - var networkChart = $scope.networkChart; - var cpuChart = $scope.cpuChart; - var memoryChart = $scope.memoryChart; - - stopRepeater(); - setUpdateRepeater(networkChart, cpuChart, memoryChart); - $('#refreshRateChange').show(); - $('#refreshRateChange').fadeOut(1500); - }; - - function startChartUpdate(networkChart, cpuChart, memoryChart) { - $q.all({ - stats: ContainerService.containerStats($transition$.params().id), - top: ContainerService.containerTop($transition$.params().id) - }) - .then(function success(data) { - var stats = data.stats; - $scope.processInfo = data.top; - if (stats.Networks.length === 0) { - $scope.state.networkStatsUnavailable = true; + if (systemDelta > 0.0 && cpuDelta > 0.0) { + cpuPercent = (cpuDelta / systemDelta) * stats.CPUCores * 100.0; } - updateNetworkChart(stats, networkChart); - updateMemoryChart(stats, memoryChart); - updateCPUChart(stats, cpuChart); - setUpdateRepeater(networkChart, cpuChart, memoryChart); - }) - .catch(function error(err) { - stopRepeater(); - Notifications.error('Failure', err, 'Unable to retrieve container statistics'); - }); - } - function setUpdateRepeater(networkChart, cpuChart, memoryChart) { - var refreshRate = $scope.state.refreshRate; - $scope.repeater = $interval(function() { + return cpuPercent; + } + + function calculateCPUPercentWindows(stats) { + var possIntervals = + stats.NumProcs * parseFloat(moment(stats.read, 'YYYY-MM-DDTHH:mm:ss.SSSSSSSSSZ').valueOf() - moment(stats.preread, 'YYYY-MM-DDTHH:mm:ss.SSSSSSSSSZ').valueOf()); + var windowsCpuUsage = 0.0; + if (possIntervals > 0) { + windowsCpuUsage = parseFloat(stats.CurrentCPUTotalUsage - stats.PreviousCPUTotalUsage) / parseFloat(possIntervals * 100); + } + return windowsCpuUsage; + } + + $scope.changeUpdateRepeater = function () { + var networkChart = $scope.networkChart; + var cpuChart = $scope.cpuChart; + var memoryChart = $scope.memoryChart; + + stopRepeater(); + setUpdateRepeater(networkChart, cpuChart, memoryChart); + $('#refreshRateChange').show(); + $('#refreshRateChange').fadeOut(1500); + }; + + function startChartUpdate(networkChart, cpuChart, memoryChart) { $q.all({ stats: ContainerService.containerStats($transition$.params().id), - top: ContainerService.containerTop($transition$.params().id) + top: ContainerService.containerTop($transition$.params().id), }) - .then(function success(data) { - var stats = data.stats; - $scope.processInfo = data.top; - updateNetworkChart(stats, networkChart); - updateMemoryChart(stats, memoryChart); - updateCPUChart(stats, cpuChart); - }) - .catch(function error(err) { - stopRepeater(); - Notifications.error('Failure', err, 'Unable to retrieve container statistics'); + .then(function success(data) { + var stats = data.stats; + $scope.processInfo = data.top; + if (stats.Networks.length === 0) { + $scope.state.networkStatsUnavailable = true; + } + updateNetworkChart(stats, networkChart); + updateMemoryChart(stats, memoryChart); + updateCPUChart(stats, cpuChart); + setUpdateRepeater(networkChart, cpuChart, memoryChart); + }) + .catch(function error(err) { + stopRepeater(); + Notifications.error('Failure', err, 'Unable to retrieve container statistics'); + }); + } + + function setUpdateRepeater(networkChart, cpuChart, memoryChart) { + var refreshRate = $scope.state.refreshRate; + $scope.repeater = $interval(function () { + $q.all({ + stats: ContainerService.containerStats($transition$.params().id), + top: ContainerService.containerTop($transition$.params().id), + }) + .then(function success(data) { + var stats = data.stats; + $scope.processInfo = data.top; + updateNetworkChart(stats, networkChart); + updateMemoryChart(stats, memoryChart); + updateCPUChart(stats, cpuChart); + }) + .catch(function error(err) { + stopRepeater(); + Notifications.error('Failure', err, 'Unable to retrieve container statistics'); + }); + }, refreshRate * 1000); + } + + function initCharts() { + var networkChartCtx = $('#networkChart'); + var networkChart = ChartService.CreateNetworkChart(networkChartCtx); + $scope.networkChart = networkChart; + + var cpuChartCtx = $('#cpuChart'); + var cpuChart = ChartService.CreateCPUChart(cpuChartCtx); + $scope.cpuChart = cpuChart; + + var memoryChartCtx = $('#memoryChart'); + var memoryChart = ChartService.CreateMemoryChart(memoryChartCtx); + $scope.memoryChart = memoryChart; + + startChartUpdate(networkChart, cpuChart, memoryChart); + } + + function initView() { + HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); + ContainerService.container($transition$.params().id) + .then(function success(data) { + $scope.container = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve container information'); + }); + + $document.ready(function () { + initCharts(); }); - }, refreshRate * 1000); - } + } - function initCharts() { - var networkChartCtx = $('#networkChart'); - var networkChart = ChartService.CreateNetworkChart(networkChartCtx); - $scope.networkChart = networkChart; - - var cpuChartCtx = $('#cpuChart'); - var cpuChart = ChartService.CreateCPUChart(cpuChartCtx); - $scope.cpuChart = cpuChart; - - var memoryChartCtx = $('#memoryChart'); - var memoryChart = ChartService.CreateMemoryChart(memoryChartCtx); - $scope.memoryChart = memoryChart; - - startChartUpdate(networkChart, cpuChart, memoryChart); - } - - function initView() { - HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); - ContainerService.container($transition$.params().id) - .then(function success(data) { - $scope.container = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve container information'); - }); - - $document.ready(function() { - initCharts(); - }); - } - - initView(); -}]); + initView(); + }, +]); diff --git a/app/docker/views/containers/stats/containerstats.html b/app/docker/views/containers/stats/containerstats.html index a14def6e2..c4d800b9a 100644 --- a/app/docker/views/containers/stats/containerstats.html +++ b/app/docker/views/containers/stats/containerstats.html @@ -1,22 +1,21 @@ - Containers > {{ container.Name|trimcontainername }} > Stats + Containers > {{ container.Name | trimcontainername }} > Stats
- - +
- This view displays real-time statistics about the container {{ container.Name|trimcontainername }} as well as a list of the running processes - inside this container. + This view displays real-time statistics about the container {{ container.Name | trimcontainername }} as well as a list of the running processes inside this + container.
@@ -40,9 +39,7 @@
- - Network stats are unavailable for this container. - + Network stats are unavailable for this container.
@@ -52,7 +49,7 @@
-
+
@@ -62,7 +59,7 @@
-
+
@@ -85,9 +82,11 @@
diff --git a/app/docker/views/dashboard/dashboard.html b/app/docker/views/dashboard/dashboard.html index b40e9bc62..59c6e44d2 100644 --- a/app/docker/views/dashboard/dashboard.html +++ b/app/docker/views/dashboard/dashboard.html @@ -10,14 +10,19 @@
+ dismiss-action="dismissInformationPanel('docker-dashboard-info-01')" +>

- Portainer is connected to a node that is part of a Swarm cluster. Some resources located on other nodes in the cluster might not be available for management, have a look - at our agent setup for more details. + Portainer is connected to a node that is part of a Swarm cluster. Some resources located on other nodes in the cluster might not be available for management, have a look at + our agent setup for more details.

@@ -38,10 +43,12 @@ {{ endpoint.Name }} - {{ endpoint.Snapshots[0].TotalCPU }} - {{ endpoint.Snapshots[0].TotalMemory | humansize }} + {{ endpoint.Snapshots[0].TotalCPU }} {{ endpoint.Snapshots[0].TotalMemory | humansize }} - - {{ info.Swarm && info.Swarm.NodeID !== '' ? 'Swarm' : 'Standalone' }} {{ info.ServerVersion }} + Agent + + - {{ info.Swarm && info.Swarm.NodeID !== '' ? 'Swarm' : 'Standalone' }} {{ info.ServerVersion }} + + Agent @@ -55,9 +62,7 @@ - - - {{ tag }}{{ $last? '' : ', ' }} - + {{ tag }}{{ $last ? '' : ', ' }} @@ -109,7 +114,7 @@

-
+
{{ containers | runningcontainers }} running
{{ containers | stoppedcontainers }} stopped
diff --git a/app/docker/views/dashboard/dashboardController.js b/app/docker/views/dashboard/dashboardController.js index 5f1efa332..9a83cdf4e 100644 --- a/app/docker/views/dashboard/dashboardController.js +++ b/app/docker/views/dashboard/dashboardController.js @@ -1,42 +1,68 @@ -angular.module('portainer.docker') -.controller('DashboardController', ['$scope', '$q', 'ContainerService', 'ImageService', 'NetworkService', 'VolumeService', 'SystemService', 'ServiceService', 'StackService', 'EndpointService', 'Notifications', 'EndpointProvider', 'StateManager', -function ($scope, $q, ContainerService, ImageService, NetworkService, VolumeService, SystemService, ServiceService, StackService, EndpointService, Notifications, EndpointProvider, StateManager) { +angular.module('portainer.docker').controller('DashboardController', [ + '$scope', + '$q', + 'ContainerService', + 'ImageService', + 'NetworkService', + 'VolumeService', + 'SystemService', + 'ServiceService', + 'StackService', + 'EndpointService', + 'Notifications', + 'EndpointProvider', + 'StateManager', + function ( + $scope, + $q, + ContainerService, + ImageService, + NetworkService, + VolumeService, + SystemService, + ServiceService, + StackService, + EndpointService, + Notifications, + EndpointProvider, + StateManager + ) { + $scope.dismissInformationPanel = function (id) { + StateManager.dismissInformationPanel(id); + }; - $scope.dismissInformationPanel = function(id) { - StateManager.dismissInformationPanel(id); - }; + $scope.offlineMode = false; - $scope.offlineMode = false; + function initView() { + var endpointMode = $scope.applicationState.endpoint.mode; + var endpointId = EndpointProvider.endpointID(); - function initView() { - var endpointMode = $scope.applicationState.endpoint.mode; - var endpointId = EndpointProvider.endpointID(); + $q.all({ + containers: ContainerService.containers(1), + images: ImageService.images(false), + volumes: VolumeService.volumes(), + networks: NetworkService.networks(true, true, true), + services: endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER' ? ServiceService.services() : [], + stacks: StackService.stacks(true, endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER', endpointId), + info: SystemService.info(), + endpoint: EndpointService.endpoint(endpointId), + }) + .then(function success(data) { + $scope.containers = data.containers; + $scope.images = data.images; + $scope.volumeCount = data.volumes.length; + $scope.networkCount = data.networks.length; + $scope.serviceCount = data.services.length; + $scope.stackCount = data.stacks.length; + $scope.info = data.info; + $scope.endpoint = data.endpoint; + $scope.offlineMode = EndpointProvider.offlineMode(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to load dashboard data'); + }); + } - $q.all({ - containers: ContainerService.containers(1), - images: ImageService.images(false), - volumes: VolumeService.volumes(), - networks: NetworkService.networks(true, true, true), - services: endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER' ? ServiceService.services() : [], - stacks: StackService.stacks(true, endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER', endpointId), - info: SystemService.info(), - endpoint: EndpointService.endpoint(endpointId) - }) - .then(function success(data) { - $scope.containers = data.containers; - $scope.images = data.images; - $scope.volumeCount = data.volumes.length; - $scope.networkCount = data.networks.length; - $scope.serviceCount = data.services.length; - $scope.stackCount = data.stacks.length; - $scope.info = data.info; - $scope.endpoint = data.endpoint; - $scope.offlineMode = EndpointProvider.offlineMode(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to load dashboard data'); - }); - } - - initView(); -}]); + initView(); + }, +]); diff --git a/app/docker/views/events/events.html b/app/docker/views/events/events.html index d67e3fc44..98caa58da 100644 --- a/app/docker/views/events/events.html +++ b/app/docker/views/events/events.html @@ -9,11 +9,6 @@
- +
diff --git a/app/docker/views/events/eventsController.js b/app/docker/views/events/eventsController.js index f0d0ef68f..8affeaf84 100644 --- a/app/docker/views/events/eventsController.js +++ b/app/docker/views/events/eventsController.js @@ -1,21 +1,23 @@ import moment from 'moment'; -angular.module('portainer.docker') -.controller('EventsController', ['$scope', 'Notifications', 'SystemService', -function ($scope, Notifications, SystemService) { +angular.module('portainer.docker').controller('EventsController', [ + '$scope', + 'Notifications', + 'SystemService', + function ($scope, Notifications, SystemService) { + function initView() { + var from = moment().subtract(24, 'hour').unix(); + var to = moment().unix(); - function initView() { - var from = moment().subtract(24, 'hour').unix(); - var to = moment().unix(); + SystemService.events(from, to) + .then(function success(data) { + $scope.events = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to load events'); + }); + } - SystemService.events(from, to) - .then(function success(data) { - $scope.events = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to load events'); - }); - } - - initView(); -}]); + initView(); + }, +]); diff --git a/app/docker/views/host/host-browser-view/host-browser-view-controller.js b/app/docker/views/host/host-browser-view/host-browser-view-controller.js index 3f45fcc48..4024fd152 100644 --- a/app/docker/views/host/host-browser-view/host-browser-view-controller.js +++ b/app/docker/views/host/host-browser-view/host-browser-view-controller.js @@ -1,17 +1,18 @@ angular.module('portainer.docker').controller('HostBrowserViewController', [ - 'SystemService', 'Notifications', + 'SystemService', + 'Notifications', function HostBrowserViewController(SystemService, Notifications) { var ctrl = this; ctrl.$onInit = $onInit; function $onInit() { SystemService.info() - .then(function onInfoLoaded(host) { - ctrl.host = host; - }) - .catch(function onError(err) { - Notifications.error('Unable to retrieve host information', err); - }); + .then(function onInfoLoaded(host) { + ctrl.host = host; + }) + .catch(function onError(err) { + Notifications.error('Unable to retrieve host information', err); + }); } - } + }, ]); diff --git a/app/docker/views/host/host-browser-view/host-browser-view.html b/app/docker/views/host/host-browser-view/host-browser-view.html index 2d87e4b2e..b8836c773 100644 --- a/app/docker/views/host/host-browser-view/host-browser-view.html +++ b/app/docker/views/host/host-browser-view/host-browser-view.html @@ -1,14 +1,12 @@ - Host > {{ $ctrl.host.Name }} > browse + Host > {{ $ctrl.host.Name }} > browse
- +
diff --git a/app/docker/views/host/host-browser-view/host-browser-view.js b/app/docker/views/host/host-browser-view/host-browser-view.js index 8d2bd1a20..4887dece2 100644 --- a/app/docker/views/host/host-browser-view/host-browser-view.js +++ b/app/docker/views/host/host-browser-view/host-browser-view.js @@ -1,4 +1,4 @@ angular.module('portainer.docker').component('hostBrowserView', { templateUrl: './host-browser-view.html', - controller: 'HostBrowserViewController' + controller: 'HostBrowserViewController', }); diff --git a/app/docker/views/host/host-job/host-job-controller.js b/app/docker/views/host/host-job/host-job-controller.js index 811509f7b..9e34700c4 100644 --- a/app/docker/views/host/host-job/host-job-controller.js +++ b/app/docker/views/host/host-job/host-job-controller.js @@ -1,17 +1,18 @@ angular.module('portainer.docker').controller('HostJobController', [ - 'SystemService', 'Notifications', + 'SystemService', + 'Notifications', function HostJobController(SystemService, Notifications) { var ctrl = this; ctrl.$onInit = $onInit; function $onInit() { SystemService.info() - .then(function onInfoLoaded(host) { - ctrl.host = host; - }) - .catch(function onError(err) { - Notifications.error('Unable to retrieve host information', err); - }); + .then(function onInfoLoaded(host) { + ctrl.host = host; + }) + .catch(function onError(err) { + Notifications.error('Unable to retrieve host information', err); + }); } - } + }, ]); diff --git a/app/docker/views/host/host-job/host-job.js b/app/docker/views/host/host-job/host-job.js index 2436075ec..c7959a63c 100644 --- a/app/docker/views/host/host-job/host-job.js +++ b/app/docker/views/host/host-job/host-job.js @@ -1,4 +1,4 @@ angular.module('portainer.docker').component('hostJobView', { templateUrl: './host-job.html', - controller: 'HostJobController' + controller: 'HostJobController', }); diff --git a/app/docker/views/host/host-view-controller.js b/app/docker/views/host/host-view-controller.js index 2bc6421b2..ebb5a6975 100644 --- a/app/docker/views/host/host-view-controller.js +++ b/app/docker/views/host/host-view-controller.js @@ -1,5 +1,12 @@ angular.module('portainer.docker').controller('HostViewController', [ - '$q', 'SystemService', 'Notifications', 'StateManager', 'AgentService', 'ContainerService', 'Authentication', 'EndpointProvider', + '$q', + 'SystemService', + 'Notifications', + 'StateManager', + 'AgentService', + 'ContainerService', + 'Authentication', + 'EndpointProvider', function HostViewController($q, SystemService, Notifications, StateManager, AgentService, ContainerService, Authentication, EndpointProvider) { var ctrl = this; @@ -7,8 +14,8 @@ angular.module('portainer.docker').controller('HostViewController', [ ctrl.state = { isAgent: false, - isAdmin : false, - offlineMode: false + isAdmin: false, + offlineMode: false, }; this.engineDetails = {}; @@ -27,28 +34,24 @@ angular.module('portainer.docker').controller('HostViewController', [ $q.all({ version: SystemService.version(), info: SystemService.info(), - jobs: ctrl.state.isAdmin ? ContainerService.containers(true, { label: ['io.portainer.job.endpoint'] }) : [] + jobs: ctrl.state.isAdmin ? ContainerService.containers(true, { label: ['io.portainer.job.endpoint'] }) : [], }) - .then(function success(data) { - ctrl.engineDetails = buildEngineDetails(data); - ctrl.hostDetails = buildHostDetails(data.info); - ctrl.state.offlineMode = EndpointProvider.offlineMode(); - ctrl.jobs = data.jobs; + .then(function success(data) { + ctrl.engineDetails = buildEngineDetails(data); + ctrl.hostDetails = buildHostDetails(data.info); + ctrl.state.offlineMode = EndpointProvider.offlineMode(); + ctrl.jobs = data.jobs; - if (ctrl.state.isAgent && agentApiVersion > 1) { - return AgentService.hostInfo(data.info.Hostname).then(function onHostInfoLoad(agentHostInfo) { - ctrl.devices = agentHostInfo.PCIDevices; - ctrl.disks = agentHostInfo.PhysicalDisks; - }); - } - }) - .catch(function error(err) { - Notifications.error( - 'Failure', - err, - 'Unable to retrieve engine details' - ); - }); + if (ctrl.state.isAgent && agentApiVersion > 1) { + return AgentService.hostInfo(data.info.Hostname).then(function onHostInfoLoad(agentHostInfo) { + ctrl.devices = agentHostInfo.PCIDevices; + ctrl.disks = agentHostInfo.PhysicalDisks; + }); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve engine details'); + }); } function buildEngineDetails(data) { @@ -61,7 +64,7 @@ angular.module('portainer.docker').controller('HostViewController', [ storageDriver: info.Driver, loggingDriver: info.LoggingDriver, volumePlugins: info.Plugins.Volume, - networkPlugins: info.Plugins.Network + networkPlugins: info.Plugins.Network, }; } @@ -70,13 +73,13 @@ angular.module('portainer.docker').controller('HostViewController', [ os: { arch: info.Architecture, type: info.OSType, - name: info.OperatingSystem + name: info.OperatingSystem, }, name: info.Name, kernelVersion: info.KernelVersion, totalCPU: info.NCPU, - totalMemory: info.MemTotal + totalMemory: info.MemTotal, }; } - } + }, ]); diff --git a/app/docker/views/host/host-view.js b/app/docker/views/host/host-view.js index d6171c666..eec3d03e5 100644 --- a/app/docker/views/host/host-view.js +++ b/app/docker/views/host/host-view.js @@ -1,4 +1,4 @@ angular.module('portainer.docker').component('hostView', { templateUrl: './host-view.html', - controller: 'HostViewController' + controller: 'HostViewController', }); diff --git a/app/docker/views/images/build/buildImageController.js b/app/docker/views/images/build/buildImageController.js index 5f4aec623..0503642ff 100644 --- a/app/docker/views/images/build/buildImageController.js +++ b/app/docker/views/images/build/buildImageController.js @@ -1,94 +1,98 @@ -angular.module('portainer.docker') -.controller('BuildImageController', ['$scope', '$state', 'BuildService', 'Notifications', 'HttpRequestHelper', -function ($scope, $state, BuildService, Notifications, HttpRequestHelper) { +angular.module('portainer.docker').controller('BuildImageController', [ + '$scope', + '$state', + 'BuildService', + 'Notifications', + 'HttpRequestHelper', + function ($scope, $state, BuildService, Notifications, HttpRequestHelper) { + $scope.state = { + BuildType: 'editor', + actionInProgress: false, + activeTab: 0, + }; - $scope.state = { - BuildType: 'editor', - actionInProgress: false, - activeTab: 0 - }; + $scope.formValues = { + ImageNames: [{ Name: '' }], + UploadFile: null, + DockerFileContent: '', + URL: '', + Path: 'Dockerfile', + NodeName: null, + }; - $scope.formValues = { - ImageNames: [{ Name: '' }], - UploadFile: null, - DockerFileContent: '', - URL: '', - Path: 'Dockerfile', - NodeName: null - }; + $scope.addImageName = function () { + $scope.formValues.ImageNames.push({ Name: '' }); + }; - $scope.addImageName = function() { - $scope.formValues.ImageNames.push({ Name: '' }); - }; + $scope.removeImageName = function (index) { + $scope.formValues.ImageNames.splice(index, 1); + }; - $scope.removeImageName = function(index) { - $scope.formValues.ImageNames.splice(index, 1); - }; + function buildImageBasedOnBuildType(method, names) { + var buildType = $scope.state.BuildType; + var dockerfilePath = $scope.formValues.Path; - function buildImageBasedOnBuildType(method, names) { - var buildType = $scope.state.BuildType; - var dockerfilePath = $scope.formValues.Path; + if (buildType === 'upload') { + var file = $scope.formValues.UploadFile; + return BuildService.buildImageFromUpload(names, file, dockerfilePath); + } else if (buildType === 'url') { + var URL = $scope.formValues.URL; + return BuildService.buildImageFromURL(names, URL, dockerfilePath); + } else { + var dockerfileContent = $scope.formValues.DockerFileContent; + return BuildService.buildImageFromDockerfileContent(names, dockerfileContent); + } + } - if (buildType === 'upload') { - var file = $scope.formValues.UploadFile; - return BuildService.buildImageFromUpload(names, file, dockerfilePath); - } else if (buildType === 'url') { - var URL = $scope.formValues.URL; - return BuildService.buildImageFromURL(names, URL, dockerfilePath); - } else { - var dockerfileContent = $scope.formValues.DockerFileContent; - return BuildService.buildImageFromDockerfileContent(names, dockerfileContent); - } - } + $scope.buildImage = function () { + var buildType = $scope.state.BuildType; - $scope.buildImage = function() { - var buildType = $scope.state.BuildType; + if (buildType === 'editor' && $scope.formValues.DockerFileContent === '') { + $scope.state.formValidationError = 'Dockerfile content must not be empty'; + return; + } - if (buildType === 'editor' && $scope.formValues.DockerFileContent === '') { - $scope.state.formValidationError = 'Dockerfile content must not be empty'; - return; - } + $scope.state.actionInProgress = true; - $scope.state.actionInProgress = true; + var imageNames = $scope.formValues.ImageNames.filter(function filterNull(x) { + return x.Name; + }).map(function getNames(x) { + return x.Name; + }); - var imageNames = $scope.formValues.ImageNames.filter(function filterNull(x) { - return x.Name; - }).map(function getNames(x) { - return x.Name; - }); + var nodeName = $scope.formValues.NodeName; + HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); - var nodeName = $scope.formValues.NodeName; - HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); + buildImageBasedOnBuildType(buildType, imageNames) + .then(function success(data) { + $scope.buildLogs = data.buildLogs; + $scope.state.activeTab = 1; + if (data.hasError) { + Notifications.error('An error occured during build', { msg: 'Please check build logs output' }); + } else { + Notifications.success('Image successfully built'); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to build image'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + }; - buildImageBasedOnBuildType(buildType, imageNames) - .then(function success(data) { - $scope.buildLogs = data.buildLogs; - $scope.state.activeTab = 1; - if (data.hasError) { - Notifications.error('An error occured during build', { msg: 'Please check build logs output' }); - } else { - Notifications.success('Image successfully built'); - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to build image'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; + $scope.validImageNames = function () { + for (var i = 0; i < $scope.formValues.ImageNames.length; i++) { + var item = $scope.formValues.ImageNames[i]; + if (item.Name !== '') { + return true; + } + } + return false; + }; - $scope.validImageNames = function() { - for (var i = 0; i < $scope.formValues.ImageNames.length; i++) { - var item = $scope.formValues.ImageNames[i]; - if (item.Name !== '') { - return true; - } - } - return false; - }; - - $scope.editorUpdate = function(cm) { - $scope.formValues.DockerFileContent = cm.getValue(); - }; -}]); + $scope.editorUpdate = function (cm) { + $scope.formValues.DockerFileContent = cm.getValue(); + }; + }, +]); diff --git a/app/docker/views/images/build/buildimage.html b/app/docker/views/images/build/buildimage.html index 7820c4b9b..2bde8f32a 100644 --- a/app/docker/views/images/build/buildimage.html +++ b/app/docker/views/images/build/buildimage.html @@ -1,8 +1,6 @@ - - Images > Build image - + Images > Build image
@@ -11,9 +9,7 @@ - - Builder - + Builder
Naming @@ -42,18 +38,21 @@
- A name must be specified in one of the following formats: name:tag, repository/name:tag or registryfqdn:port/repository/name:tag format. If you omit the tag the default latest value is assumed. + A name must be specified in one of the following formats: name:tag, repository/name:tag or + registryfqdn:port/repository/name:tag format. If you omit the tag the default latest value is assumed.
-
+
name - - + +
@@ -74,10 +73,10 @@ Build method
-
+
- +
- Specify the URL to a Dockerfile, a tarball or a public Git repository (suffixed by .git). When using a tarball or a Git repository URL, the root folder will be used as the build context. + Specify the URL to a Dockerfile, a tarball or a public Git repository (suffixed by .git). When using a tarball or a Git repository URL, the root folder + will be used as the build context.
- +
@@ -189,7 +191,7 @@
- +
@@ -199,9 +201,7 @@ Deployment
- - +
@@ -210,12 +210,16 @@
- @@ -226,9 +230,7 @@ - - Output - + Output
               

{{ line }}

No build output available.

diff --git a/app/docker/views/images/edit/image.html b/app/docker/views/images/edit/image.html index 5dcc3ba26..ba3de195a 100644 --- a/app/docker/views/images/edit/image.html +++ b/app/docker/views/images/edit/image.html @@ -1,4 +1,4 @@ - + Images > {{ image.Id }} @@ -13,8 +13,8 @@
-
-
+
+
{{ tag }} @@ -34,9 +34,9 @@
- Note: you can click on the upload icon to push an image - or on the download icon to pull an image - or on the trash icon to delete a tag. + Note: you can click on the upload icon to push an image or on the download icon + to pull an image or on the trash icon to delete a + tag.
@@ -63,10 +63,7 @@ - +
@@ -97,29 +94,39 @@ ID {{ image.Id }} - - + Parent - {{ image.Parent }} + {{ image.Parent }} Size - {{ image.VirtualSize|humansize }} + {{ image.VirtualSize | humansize }} Created - {{ image.Created|getisodate }} + {{ image.Created | getisodate }} Build - Docker {{ image.DockerVersion }} on {{ image.Os}}, {{ image.Architecture }} + Docker {{ image.DockerVersion }} on {{ image.Os }}, {{ image.Architecture }} Author @@ -141,11 +148,15 @@ CMD - {{ image.Command|command }} + {{ image.Command | command }} ENTRYPOINT - {{ image.Entrypoint|command }} + {{ image.Entrypoint | command }} EXPOSE @@ -179,7 +190,6 @@
-
@@ -188,7 +198,7 @@ - - - @@ -56,7 +58,8 @@ resource-id="network.Id" resource-control="network.ResourceControl" resource-type="'network'" - disable-ownership-change="isSystemNetwork()"> + disable-ownership-change="isSystemNetwork()" +> @@ -78,7 +81,6 @@ -
@@ -94,12 +96,16 @@
- + diff --git a/app/docker/views/networks/edit/networkController.js b/app/docker/views/networks/edit/networkController.js index 294c76c5e..06431ad1a 100644 --- a/app/docker/views/networks/edit/networkController.js +++ b/app/docker/views/networks/edit/networkController.js @@ -1,96 +1,112 @@ -angular.module('portainer.docker') -.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper', 'NetworkHelper', -function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper, NetworkHelper) { +angular.module('portainer.docker').controller('NetworkController', [ + '$scope', + '$state', + '$transition$', + '$filter', + 'NetworkService', + 'Container', + 'Notifications', + 'HttpRequestHelper', + 'NetworkHelper', + function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper, NetworkHelper) { + $scope.removeNetwork = function removeNetwork() { + NetworkService.remove($transition$.params().id, $transition$.params().id) + .then(function success() { + Notifications.success('Network removed', $transition$.params().id); + $state.go('docker.networks', {}); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove network'); + }); + }; - $scope.removeNetwork = function removeNetwork() { - NetworkService.remove($transition$.params().id, $transition$.params().id) - .then(function success() { - Notifications.success('Network removed', $transition$.params().id); - $state.go('docker.networks', {}); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove network'); - }); - }; + $scope.containerLeaveNetwork = function containerLeaveNetwork(network, container) { + HttpRequestHelper.setPortainerAgentTargetHeader(container.NodeName); + NetworkService.disconnectContainer($transition$.params().id, container.Id, false) + .then(function success() { + Notifications.success('Container left network', $transition$.params().id); + $state.go('docker.networks.network', { id: network.Id }, { reload: true }); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to disconnect container from network'); + }); + }; - $scope.containerLeaveNetwork = function containerLeaveNetwork(network, container) { - HttpRequestHelper.setPortainerAgentTargetHeader(container.NodeName); - NetworkService.disconnectContainer($transition$.params().id, container.Id, false) - .then(function success() { - Notifications.success('Container left network', $transition$.params().id); - $state.go('docker.networks.network', { id: network.Id }, { reload: true }); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to disconnect container from network'); - }); - }; + $scope.isSystemNetwork = function () { + return $scope.network && NetworkHelper.isSystemNetwork($scope.network); + }; - $scope.isSystemNetwork = function() { - return $scope.network && NetworkHelper.isSystemNetwork($scope.network); - } + $scope.allowRemove = function () { + return !$scope.isSystemNetwork(); + }; - $scope.allowRemove = function() { - return !$scope.isSystemNetwork(); - }; - - function filterContainersInNetwork(network, containers) { - var containersInNetwork = []; - containers.forEach(function(container) { - var containerInNetwork = network.Containers[container.Id]; - if (containerInNetwork) { - containerInNetwork.Id = container.Id; - // Name is not available in Docker 1.9 - if (!containerInNetwork.Name) { - containerInNetwork.Name = $filter('trimcontainername')(container.Names[0]); + function filterContainersInNetwork(network, containers) { + var containersInNetwork = []; + containers.forEach(function (container) { + var containerInNetwork = network.Containers[container.Id]; + if (containerInNetwork) { + containerInNetwork.Id = container.Id; + // Name is not available in Docker 1.9 + if (!containerInNetwork.Name) { + containerInNetwork.Name = $filter('trimcontainername')(container.Names[0]); + } + containersInNetwork.push(containerInNetwork); } - containersInNetwork.push(containerInNetwork); - } - }); - $scope.containersInNetwork = containersInNetwork; - } + }); + $scope.containersInNetwork = containersInNetwork; + } - function getContainersInNetwork(network) { - var apiVersion = $scope.applicationState.endpoint.apiVersion; - if (network.Containers) { - if (apiVersion < 1.24) { - Container.query({}, function success(data) { - var containersInNetwork = data.filter(function filter(container) { - if (container.HostConfig.NetworkMode === network.Name) { - return container; + function getContainersInNetwork(network) { + var apiVersion = $scope.applicationState.endpoint.apiVersion; + if (network.Containers) { + if (apiVersion < 1.24) { + Container.query( + {}, + function success(data) { + var containersInNetwork = data.filter(function filter(container) { + if (container.HostConfig.NetworkMode === network.Name) { + return container; + } + }); + filterContainersInNetwork(network, containersInNetwork); + }, + function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve containers in network'); } - }); - filterContainersInNetwork(network, containersInNetwork); - }, function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve containers in network'); - }); - } else { - Container.query({ - filters: { network: [$transition$.params().id] } - }, function success(data) { - filterContainersInNetwork(network, data); - }, function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve containers in network'); - }); + ); + } else { + Container.query( + { + filters: { network: [$transition$.params().id] }, + }, + function success(data) { + filterContainersInNetwork(network, data); + }, + function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve containers in network'); + } + ); + } } } - } - function initView() { - var nodeName = $transition$.params().nodeName; - HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); - $scope.nodeName = nodeName; - NetworkService.network($transition$.params().id) - .then(function success(data) { - $scope.network = data; - var endpointProvider = $scope.applicationState.endpoint.mode.provider; - if (endpointProvider !== 'VMWARE_VIC') { - getContainersInNetwork(data); - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve network info'); - }); - } + function initView() { + var nodeName = $transition$.params().nodeName; + HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); + $scope.nodeName = nodeName; + NetworkService.network($transition$.params().id) + .then(function success(data) { + $scope.network = data; + var endpointProvider = $scope.applicationState.endpoint.mode.provider; + if (endpointProvider !== 'VMWARE_VIC') { + getContainersInNetwork(data); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve network info'); + }); + } - initView(); -}]); + initView(); + }, +]); diff --git a/app/docker/views/networks/networks.html b/app/docker/views/networks/networks.html index 497d95cde..737063089 100644 --- a/app/docker/views/networks/networks.html +++ b/app/docker/views/networks/networks.html @@ -10,14 +10,16 @@
diff --git a/app/docker/views/networks/networksController.js b/app/docker/views/networks/networksController.js index 6beb8da73..be66b64b9 100644 --- a/app/docker/views/networks/networksController.js +++ b/app/docker/views/networks/networksController.js @@ -1,80 +1,87 @@ import _ from 'lodash-es'; -angular.module('portainer.docker') -.controller('NetworksController', ['$q', '$scope', '$state', 'NetworkService', 'Notifications', 'HttpRequestHelper', 'EndpointProvider', 'AgentService', -function ($q, $scope, $state, NetworkService, Notifications, HttpRequestHelper, EndpointProvider, AgentService) { - - $scope.removeAction = function (selectedItems) { - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (network) { - HttpRequestHelper.setPortainerAgentTargetHeader(network.NodeName); - NetworkService.remove(network.Id) - .then(function success() { - Notifications.success('Network successfully removed', network.Name); - var index = $scope.networks.indexOf(network); - $scope.networks.splice(index, 1); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove network'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } +angular.module('portainer.docker').controller('NetworksController', [ + '$q', + '$scope', + '$state', + 'NetworkService', + 'Notifications', + 'HttpRequestHelper', + 'EndpointProvider', + 'AgentService', + function ($q, $scope, $state, NetworkService, Notifications, HttpRequestHelper, EndpointProvider, AgentService) { + $scope.removeAction = function (selectedItems) { + var actionCount = selectedItems.length; + angular.forEach(selectedItems, function (network) { + HttpRequestHelper.setPortainerAgentTargetHeader(network.NodeName); + NetworkService.remove(network.Id) + .then(function success() { + Notifications.success('Network successfully removed', network.Name); + var index = $scope.networks.indexOf(network); + $scope.networks.splice(index, 1); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove network'); + }) + .finally(function final() { + --actionCount; + if (actionCount === 0) { + $state.reload(); + } + }); }); - }); - }; - - $scope.offlineMode = false; - - $scope.getNetworks = getNetworks; - - function groupSwarmNetworksManagerNodesFirst(networks, agents) { - const getRole = (item) => _.find(agents, (agent) => agent.NodeName === item.NodeName).NodeRole; - - const nonSwarmNetworks = _.remove(networks, (item) => item.Scope !== 'swarm') - const grouped = _.toArray(_.groupBy(networks, (item) => item.Id)); - const sorted = _.map(grouped, (arr) => _.sortBy(arr, (item) => getRole(item))); - const arr = _.map(sorted, (a) => { - const item = a[0]; - for (let i = 1; i < a.length; i++) { - item.Subs.push(a[i]); - } - return item; - }); - const res = _.concat(arr, ...nonSwarmNetworks); - return res; - } - - function getNetworks() { - const req = { - networks: NetworkService.networks(true, true, true) }; - if ($scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { - req.agents = AgentService.agents(); + $scope.offlineMode = false; + + $scope.getNetworks = getNetworks; + + function groupSwarmNetworksManagerNodesFirst(networks, agents) { + const getRole = (item) => _.find(agents, (agent) => agent.NodeName === item.NodeName).NodeRole; + + const nonSwarmNetworks = _.remove(networks, (item) => item.Scope !== 'swarm'); + const grouped = _.toArray(_.groupBy(networks, (item) => item.Id)); + const sorted = _.map(grouped, (arr) => _.sortBy(arr, (item) => getRole(item))); + const arr = _.map(sorted, (a) => { + const item = a[0]; + for (let i = 1; i < a.length; i++) { + item.Subs.push(a[i]); + } + return item; + }); + const res = _.concat(arr, ...nonSwarmNetworks); + return res; } - $q.all(req) - .then((data) => { - $scope.offlineMode = EndpointProvider.offlineMode(); - const networks = _.forEach(data.networks, (item) => item.Subs = []); + function getNetworks() { + const req = { + networks: NetworkService.networks(true, true, true), + }; + if ($scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { - $scope.networks = groupSwarmNetworksManagerNodesFirst(data.networks, data.agents); - } else { - $scope.networks = networks; + req.agents = AgentService.agents(); } - }) - .catch((err) => { - $scope.networks = []; - Notifications.error('Failure', err, 'Unable to retrieve networks'); - }); - } - function initView() { - getNetworks(); - } + $q.all(req) + .then((data) => { + $scope.offlineMode = EndpointProvider.offlineMode(); + const networks = _.forEach(data.networks, (item) => (item.Subs = [])); + if ($scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { + $scope.networks = groupSwarmNetworksManagerNodesFirst(data.networks, data.agents); + } else { + $scope.networks = networks; + } + }) + .catch((err) => { + $scope.networks = []; + Notifications.error('Failure', err, 'Unable to retrieve networks'); + }); + } - initView(); -}]); + function initView() { + getNetworks(); + } + + initView(); + }, +]); diff --git a/app/docker/views/nodes/node-browser/node-browser-controller.js b/app/docker/views/nodes/node-browser/node-browser-controller.js index 5c55c3e1b..e34fcef5a 100644 --- a/app/docker/views/nodes/node-browser/node-browser-controller.js +++ b/app/docker/views/nodes/node-browser/node-browser-controller.js @@ -1,5 +1,8 @@ angular.module('portainer.docker').controller('NodeBrowserController', [ - '$stateParams', 'NodeService', 'HttpRequestHelper', 'Notifications', + '$stateParams', + 'NodeService', + 'HttpRequestHelper', + 'Notifications', function NodeBrowserController($stateParams, NodeService, HttpRequestHelper, Notifications) { var ctrl = this; ctrl.$onInit = $onInit; @@ -8,13 +11,13 @@ angular.module('portainer.docker').controller('NodeBrowserController', [ ctrl.nodeId = $stateParams.id; NodeService.node(ctrl.nodeId) - .then(function onNodeLoaded(node) { - HttpRequestHelper.setPortainerAgentTargetHeader(node.Hostname); - ctrl.node = node; - }) - .catch(function onError(err) { - Notifications.error('Unable to retrieve host information', err); - }); + .then(function onNodeLoaded(node) { + HttpRequestHelper.setPortainerAgentTargetHeader(node.Hostname); + ctrl.node = node; + }) + .catch(function onError(err) { + Notifications.error('Unable to retrieve host information', err); + }); } - } + }, ]); diff --git a/app/docker/views/nodes/node-browser/node-browser.html b/app/docker/views/nodes/node-browser/node-browser.html index 2edeae199..7e9dfe3ce 100644 --- a/app/docker/views/nodes/node-browser/node-browser.html +++ b/app/docker/views/nodes/node-browser/node-browser.html @@ -1,14 +1,12 @@ - Swarm > {{ $ctrl.node.Hostname }} > browse + Swarm > {{ $ctrl.node.Hostname }} > browse
- +
diff --git a/app/docker/views/nodes/node-browser/node-browser.js b/app/docker/views/nodes/node-browser/node-browser.js index a43fa97fe..b073a2a40 100644 --- a/app/docker/views/nodes/node-browser/node-browser.js +++ b/app/docker/views/nodes/node-browser/node-browser.js @@ -1,4 +1,4 @@ angular.module('portainer.docker').component('nodeBrowserView', { templateUrl: './node-browser.html', - controller: 'NodeBrowserController' + controller: 'NodeBrowserController', }); diff --git a/app/docker/views/nodes/node-details/node-details-view-controller.js b/app/docker/views/nodes/node-details/node-details-view-controller.js index 3cc0a6e90..020c3b586 100644 --- a/app/docker/views/nodes/node-details/node-details-view-controller.js +++ b/app/docker/views/nodes/node-details/node-details-view-controller.js @@ -1,5 +1,11 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [ - '$q', '$stateParams', 'NodeService', 'StateManager', 'AgentService', 'ContainerService', 'Authentication', + '$q', + '$stateParams', + 'NodeService', + 'StateManager', + 'AgentService', + 'ContainerService', + 'Authentication', function NodeDetailsViewController($q, $stateParams, NodeService, StateManager, AgentService, ContainerService, Authentication) { var ctrl = this; @@ -7,7 +13,7 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [ ctrl.state = { isAgent: false, - isAdmin: false + isAdmin: false, }; function initView() { @@ -21,9 +27,8 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [ var nodeId = $stateParams.id; $q.all({ node: NodeService.node(nodeId), - jobs: fetchJobs ? ContainerService.containers(true, { label: ['io.portainer.job.endpoint'] }) : [] - }) - .then(function (data) { + jobs: fetchJobs ? ContainerService.containers(true, { label: ['io.portainer.job.endpoint'] }) : [], + }).then(function (data) { var node = data.node; ctrl.originalNode = node; ctrl.hostDetails = buildHostDetails(node); @@ -37,8 +42,7 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [ return; } - AgentService.hostInfo(node.Hostname) - .then(function onHostInfoLoad(agentHostInfo) { + AgentService.hostInfo(node.Hostname).then(function onHostInfoLoad(agentHostInfo) { ctrl.devices = agentHostInfo.PCIDevices; ctrl.disks = agentHostInfo.PhysicalDisks; }); @@ -50,11 +54,11 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [ return { os: { arch: node.PlatformArchitecture, - type: node.PlatformOS + type: node.PlatformOS, }, name: node.Hostname, totalCPU: node.CPUs / 1e9, - totalMemory: node.Memory + totalMemory: node.Memory, }; } @@ -74,18 +78,18 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [ managerAddress: node.ManagerAddr, availability: node.Availability, status: node.Status, - nodeLabels: node.Labels + nodeLabels: node.Labels, }; } function transformPlugins(pluginsList, type) { return pluginsList - .filter(function(plugin) { - return plugin.Type === type; - }) - .map(function(plugin) { - return plugin.Name; - }); + .filter(function (plugin) { + return plugin.Type === type; + }) + .map(function (plugin) { + return plugin.Name; + }); } - } + }, ]); diff --git a/app/docker/views/nodes/node-details/node-details-view.html b/app/docker/views/nodes/node-details/node-details-view.html index c2dd7c7e2..571d2dde0 100644 --- a/app/docker/views/nodes/node-details/node-details-view.html +++ b/app/docker/views/nodes/node-details/node-details-view.html @@ -12,8 +12,5 @@ job-url="docker.nodes.node.job" jobs="$ctrl.jobs" > - + diff --git a/app/docker/views/nodes/node-details/node-details-view.js b/app/docker/views/nodes/node-details/node-details-view.js index e0ba268cf..929b4f4ad 100644 --- a/app/docker/views/nodes/node-details/node-details-view.js +++ b/app/docker/views/nodes/node-details/node-details-view.js @@ -1,4 +1,4 @@ angular.module('portainer.docker').component('nodeDetailsView', { templateUrl: './node-details-view.html', - controller: 'NodeDetailsViewController' + controller: 'NodeDetailsViewController', }); diff --git a/app/docker/views/nodes/node-job/node-job-controller.js b/app/docker/views/nodes/node-job/node-job-controller.js index 9f1173d09..f4f47d363 100644 --- a/app/docker/views/nodes/node-job/node-job-controller.js +++ b/app/docker/views/nodes/node-job/node-job-controller.js @@ -1,5 +1,8 @@ angular.module('portainer.docker').controller('NodeJobController', [ - '$stateParams', 'NodeService', 'HttpRequestHelper', 'Notifications', + '$stateParams', + 'NodeService', + 'HttpRequestHelper', + 'Notifications', function NodeJobController($stateParams, NodeService, HttpRequestHelper, Notifications) { var ctrl = this; ctrl.$onInit = $onInit; @@ -8,13 +11,13 @@ angular.module('portainer.docker').controller('NodeJobController', [ ctrl.nodeId = $stateParams.id; NodeService.node(ctrl.nodeId) - .then(function onNodeLoaded(node) { - HttpRequestHelper.setPortainerAgentTargetHeader(node.Hostname); - ctrl.node = node; - }) - .catch(function onError(err) { - Notifications.error('Unable to retrieve host information', err); - }); + .then(function onNodeLoaded(node) { + HttpRequestHelper.setPortainerAgentTargetHeader(node.Hostname); + ctrl.node = node; + }) + .catch(function onError(err) { + Notifications.error('Unable to retrieve host information', err); + }); } - } + }, ]); diff --git a/app/docker/views/nodes/node-job/node-job.html b/app/docker/views/nodes/node-job/node-job.html index 90ae92d14..a842fa746 100644 --- a/app/docker/views/nodes/node-job/node-job.html +++ b/app/docker/views/nodes/node-job/node-job.html @@ -1,7 +1,7 @@ - Swarm > {{ $ctrl.node.Hostname }} > execute job + Swarm > {{ $ctrl.node.Hostname }} > execute job @@ -9,9 +9,7 @@
- +
diff --git a/app/docker/views/nodes/node-job/node-job.js b/app/docker/views/nodes/node-job/node-job.js index 334752fff..b659b4e71 100644 --- a/app/docker/views/nodes/node-job/node-job.js +++ b/app/docker/views/nodes/node-job/node-job.js @@ -1,4 +1,4 @@ angular.module('portainer.docker').component('nodeJobView', { templateUrl: './node-job.html', - controller: 'NodeJobController' + controller: 'NodeJobController', }); diff --git a/app/docker/views/secrets/create/createSecretController.js b/app/docker/views/secrets/create/createSecretController.js index b488ed37d..cbd654c77 100644 --- a/app/docker/views/secrets/create/createSecretController.js +++ b/app/docker/views/secrets/create/createSecretController.js @@ -1,89 +1,95 @@ import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel'; -angular.module('portainer.docker') -.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator', -function ($scope, $state, Notifications, SecretService, LabelHelper, Authentication, ResourceControlService, FormValidator) { +angular.module('portainer.docker').controller('CreateSecretController', [ + '$scope', + '$state', + 'Notifications', + 'SecretService', + 'LabelHelper', + 'Authentication', + 'ResourceControlService', + 'FormValidator', + function ($scope, $state, Notifications, SecretService, LabelHelper, Authentication, ResourceControlService, FormValidator) { + $scope.formValues = { + Name: '', + Data: '', + Labels: [], + encodeSecret: true, + AccessControlData: new AccessControlFormData(), + }; - $scope.formValues = { - Name: '', - Data: '', - Labels: [], - encodeSecret: true, - AccessControlData: new AccessControlFormData() - }; + $scope.state = { + formValidationError: '', + actionInProgress: false, + }; - $scope.state = { - formValidationError: '', - actionInProgress: false - }; + $scope.addLabel = function () { + $scope.formValues.Labels.push({ key: '', value: '' }); + }; - $scope.addLabel = function() { - $scope.formValues.Labels.push({ key: '', value: ''}); - }; + $scope.removeLabel = function (index) { + $scope.formValues.Labels.splice(index, 1); + }; - $scope.removeLabel = function(index) { - $scope.formValues.Labels.splice(index, 1); - }; - - function prepareLabelsConfig(config) { - config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels); - } - - function prepareSecretData(config) { - if ($scope.formValues.encodeSecret) { - config.Data = btoa(unescape(encodeURIComponent($scope.formValues.Data))); - } else { - config.Data = $scope.formValues.Data; - } - } - - function prepareConfiguration() { - var config = {}; - config.Name = $scope.formValues.Name; - prepareSecretData(config); - prepareLabelsConfig(config); - return config; - } - - function validateForm(accessControlData, isAdmin) { - $scope.state.formValidationError = ''; - var error = ''; - error = FormValidator.validateAccessControl(accessControlData, isAdmin); - - if (error) { - $scope.state.formValidationError = error; - return false; - } - return true; - } - - $scope.create = function () { - - const accessControlData = $scope.formValues.AccessControlData; - const userDetails = Authentication.getUserDetails(); - const isAdmin = Authentication.isAdmin(); - - if (!validateForm(accessControlData, isAdmin)) { - return; + function prepareLabelsConfig(config) { + config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels); } - $scope.state.actionInProgress = true; - var secretConfiguration = prepareConfiguration(); - SecretService.create(secretConfiguration) - .then(function success(data) { - const userId = userDetails.ID; - const resourceControl = data.Portainer.ResourceControl; - return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); - }) - .then(function success() { - Notifications.success('Secret successfully created'); - $state.go('docker.secrets', {}, {reload: true}); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create secret'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; -}]); + function prepareSecretData(config) { + if ($scope.formValues.encodeSecret) { + config.Data = btoa(unescape(encodeURIComponent($scope.formValues.Data))); + } else { + config.Data = $scope.formValues.Data; + } + } + + function prepareConfiguration() { + var config = {}; + config.Name = $scope.formValues.Name; + prepareSecretData(config); + prepareLabelsConfig(config); + return config; + } + + function validateForm(accessControlData, isAdmin) { + $scope.state.formValidationError = ''; + var error = ''; + error = FormValidator.validateAccessControl(accessControlData, isAdmin); + + if (error) { + $scope.state.formValidationError = error; + return false; + } + return true; + } + + $scope.create = function () { + const accessControlData = $scope.formValues.AccessControlData; + const userDetails = Authentication.getUserDetails(); + const isAdmin = Authentication.isAdmin(); + + if (!validateForm(accessControlData, isAdmin)) { + return; + } + + $scope.state.actionInProgress = true; + var secretConfiguration = prepareConfiguration(); + SecretService.create(secretConfiguration) + .then(function success(data) { + const userId = userDetails.ID; + const resourceControl = data.Portainer.ResourceControl; + return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); + }) + .then(function success() { + Notifications.success('Secret successfully created'); + $state.go('docker.secrets', {}, { reload: true }); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to create secret'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + }; + }, +]); diff --git a/app/docker/views/secrets/create/createsecret.html b/app/docker/views/secrets/create/createsecret.html index 167e373f7..fae2453fa 100644 --- a/app/docker/views/secrets/create/createsecret.html +++ b/app/docker/views/secrets/create/createsecret.html @@ -1,8 +1,6 @@ - - Secrets > Add secret - + Secrets > Add secret
@@ -14,7 +12,7 @@
- +
@@ -33,9 +31,7 @@ Encode secret - +
@@ -43,20 +39,18 @@
- - add label - + add label
name - +
value - +
- diff --git a/app/docker/views/secrets/edit/secret.html b/app/docker/views/secrets/edit/secret.html index 857820d18..6375294eb 100644 --- a/app/docker/views/secrets/edit/secret.html +++ b/app/docker/views/secrets/edit/secret.html @@ -24,7 +24,9 @@
@@ -54,10 +56,6 @@ - + diff --git a/app/docker/views/secrets/edit/secretController.js b/app/docker/views/secrets/edit/secretController.js index 15aecfc1f..69fa6077c 100644 --- a/app/docker/views/secrets/edit/secretController.js +++ b/app/docker/views/secrets/edit/secretController.js @@ -1,27 +1,31 @@ -angular.module('portainer.docker') -.controller('SecretController', ['$scope', '$transition$', '$state', 'SecretService', 'Notifications', -function ($scope, $transition$, $state, SecretService, Notifications) { +angular.module('portainer.docker').controller('SecretController', [ + '$scope', + '$transition$', + '$state', + 'SecretService', + 'Notifications', + function ($scope, $transition$, $state, SecretService, Notifications) { + $scope.removeSecret = function removeSecret(secretId) { + SecretService.remove(secretId) + .then(function success() { + Notifications.success('Secret successfully removed'); + $state.go('docker.secrets', {}); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove secret'); + }); + }; - $scope.removeSecret = function removeSecret(secretId) { - SecretService.remove(secretId) - .then(function success() { - Notifications.success('Secret successfully removed'); - $state.go('docker.secrets', {}); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove secret'); - }); - }; + function initView() { + SecretService.secret($transition$.params().id) + .then(function success(data) { + $scope.secret = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve secret details'); + }); + } - function initView() { - SecretService.secret($transition$.params().id) - .then(function success(data) { - $scope.secret = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve secret details'); - }); - } - - initView(); -}]); + initView(); + }, +]); diff --git a/app/docker/views/secrets/secrets.html b/app/docker/views/secrets/secrets.html index 76c8205d7..a6ddde5af 100644 --- a/app/docker/views/secrets/secrets.html +++ b/app/docker/views/secrets/secrets.html @@ -10,12 +10,14 @@
diff --git a/app/docker/views/secrets/secretsController.js b/app/docker/views/secrets/secretsController.js index 98f44e5e3..69d22b196 100644 --- a/app/docker/views/secrets/secretsController.js +++ b/app/docker/views/secrets/secretsController.js @@ -1,44 +1,47 @@ -angular.module('portainer.docker') -.controller('SecretsController', ['$scope', '$state', 'SecretService', 'Notifications', -function ($scope, $state, SecretService, Notifications) { - - $scope.removeAction = function (selectedItems) { - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (secret) { - SecretService.remove(secret.Id) - .then(function success() { - Notifications.success('Secret successfully removed', secret.Name); - var index = $scope.secrets.indexOf(secret); - $scope.secrets.splice(index, 1); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove secret'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } +angular.module('portainer.docker').controller('SecretsController', [ + '$scope', + '$state', + 'SecretService', + 'Notifications', + function ($scope, $state, SecretService, Notifications) { + $scope.removeAction = function (selectedItems) { + var actionCount = selectedItems.length; + angular.forEach(selectedItems, function (secret) { + SecretService.remove(secret.Id) + .then(function success() { + Notifications.success('Secret successfully removed', secret.Name); + var index = $scope.secrets.indexOf(secret); + $scope.secrets.splice(index, 1); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove secret'); + }) + .finally(function final() { + --actionCount; + if (actionCount === 0) { + $state.reload(); + } + }); }); - }); - }; + }; - $scope.getSecrets = getSecrets; + $scope.getSecrets = getSecrets; - function getSecrets() { - SecretService.secrets() - .then(function success(data) { - $scope.secrets = data; - }) - .catch(function error(err) { - $scope.secrets = []; - Notifications.error('Failure', err, 'Unable to retrieve secrets'); - }); - } + function getSecrets() { + SecretService.secrets() + .then(function success(data) { + $scope.secrets = data; + }) + .catch(function error(err) { + $scope.secrets = []; + Notifications.error('Failure', err, 'Unable to retrieve secrets'); + }); + } - function initView() { - getSecrets(); - } + function initView() { + getSecrets(); + } - initView(); -}]); + initView(); + }, +]); diff --git a/app/docker/views/services/create/createServiceController.js b/app/docker/views/services/create/createServiceController.js index 477fb6f65..212519bd5 100644 --- a/app/docker/views/services/create/createServiceController.js +++ b/app/docker/views/services/create/createServiceController.js @@ -2,529 +2,584 @@ import _ from 'lodash-es'; import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; -require('./includes/update-restart.html') -require('./includes/secret.html') -require('./includes/config.html') -require('./includes/resources-placement.html') +require('./includes/update-restart.html'); +require('./includes/secret.html'); +require('./includes/config.html'); +require('./includes/resources-placement.html'); -angular.module('portainer.docker') -.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'ConfigService', 'ConfigHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'PluginService', 'RegistryService', 'HttpRequestHelper', 'NodeService', 'SettingsService', 'WebhookService','EndpointProvider', -function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, ConfigHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, PluginService, RegistryService, HttpRequestHelper, NodeService, SettingsService, WebhookService,EndpointProvider) { - - $scope.formValues = { - Name: '', - RegistryModel: new PorImageRegistryModel(), - Mode: 'replicated', - Replicas: 1, - Command: '', - EntryPoint: '', - WorkingDir: '', - User: '', - Env: [], - Labels: [], - ContainerLabels: [], - Volumes: [], - Network: '', - ExtraNetworks: [], - HostsEntries: [], - Ports: [], - Parallelism: 1, - PlacementConstraints: [], - PlacementPreferences: [], - UpdateDelay: '0s', - UpdateOrder: 'stop-first', - FailureAction: 'pause', - Secrets: [], - Configs: [], - AccessControlData: new AccessControlFormData(), - CpuLimit: 0, - CpuReservation: 0, - MemoryLimit: 0, - MemoryReservation: 0, - MemoryLimitUnit: 'MB', - MemoryReservationUnit: 'MB', - RestartCondition: 'any', - RestartDelay: '5s', - RestartMaxAttempts: 0, - RestartWindow: '0s', - LogDriverName: '', - LogDriverOpts: [], - Webhook: false - }; - - $scope.state = { - formValidationError: '', - actionInProgress: false - }; - - $scope.refreshSlider = function () { - $timeout(function () { - $scope.$broadcast('rzSliderForceRender'); - }); - }; - - $scope.addPortBinding = function() { - $scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' }); - }; - - $scope.removePortBinding = function(index) { - $scope.formValues.Ports.splice(index, 1); - }; - - $scope.addExtraNetwork = function() { - $scope.formValues.ExtraNetworks.push({ Name: '' }); - }; - - $scope.removeExtraNetwork = function(index) { - $scope.formValues.ExtraNetworks.splice(index, 1); - }; - - $scope.addHostsEntry = function() { - $scope.formValues.HostsEntries.push({}); - }; - - $scope.removeHostsEntry = function(index) { - $scope.formValues.HostsEntries.splice(index, 1); - }; - - $scope.addVolume = function() { - $scope.formValues.Volumes.push({ Source: '', Target: '', ReadOnly: false, Type: 'volume' }); - }; - - $scope.removeVolume = function(index) { - $scope.formValues.Volumes.splice(index, 1); - }; - - $scope.addConfig = function() { - $scope.formValues.Configs.push({}); - }; - - $scope.removeConfig = function(index) { - $scope.formValues.Configs.splice(index, 1); - }; - - $scope.addSecret = function() { - $scope.formValues.Secrets.push({ overrideTarget: false }); - }; - - $scope.removeSecret = function(index) { - $scope.formValues.Secrets.splice(index, 1); - }; - - $scope.addEnvironmentVariable = function() { - $scope.formValues.Env.push({ name: '', value: ''}); - }; - - $scope.removeEnvironmentVariable = function(index) { - $scope.formValues.Env.splice(index, 1); - }; - - $scope.addPlacementConstraint = function() { - $scope.formValues.PlacementConstraints.push({ key: '', operator: '==', value: '' }); - }; - - $scope.removePlacementConstraint = function(index) { - $scope.formValues.PlacementConstraints.splice(index, 1); - }; - - $scope.addPlacementPreference = function() { - $scope.formValues.PlacementPreferences.push({ strategy: 'spread', value: '' }); - }; - - $scope.removePlacementPreference = function(index) { - $scope.formValues.PlacementPreferences.splice(index, 1); - }; - - $scope.addLabel = function() { - $scope.formValues.Labels.push({ key: '', value: ''}); - }; - - $scope.removeLabel = function(index) { - $scope.formValues.Labels.splice(index, 1); - }; - - $scope.addContainerLabel = function() { - $scope.formValues.ContainerLabels.push({ key: '', value: ''}); - }; - - $scope.removeContainerLabel = function(index) { - $scope.formValues.ContainerLabels.splice(index, 1); - }; - - $scope.addLogDriverOpt = function() { - $scope.formValues.LogDriverOpts.push({ name: '', value: ''}); - }; - - $scope.removeLogDriverOpt = function(index) { - $scope.formValues.LogDriverOpts.splice(index, 1); - }; - - function prepareImageConfig(config, input) { - var imageConfig = ImageHelper.createImageConfigForContainer(input.RegistryModel); - config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage; - } - - function preparePortsConfig(config, input) { - var ports = []; - input.Ports.forEach(function (binding) { - var port = { - Protocol: binding.Protocol, - PublishMode: binding.PublishMode - }; - if (binding.TargetPort) { - port.TargetPort = +binding.TargetPort; - if (binding.PublishedPort) { - port.PublishedPort = +binding.PublishedPort; - } - ports.push(port); - } - }); - config.EndpointSpec.Ports = ports; - } - - function prepareSchedulingConfig(config, input) { - if (input.Mode === 'replicated') { - config.Mode.Replicated = { - Replicas: input.Replicas - }; - } else { - config.Mode.Global = {}; - } - } - - function commandToArray(cmd) { - var tokens = [].concat.apply([], cmd.split('\'').map(function(v,i) { - return i%2 ? v : v.split(' '); - })).filter(Boolean); - return tokens; - } - - function prepareCommandConfig(config, input) { - if (input.EntryPoint) { - config.TaskTemplate.ContainerSpec.Command = commandToArray(input.EntryPoint); - } - if (input.Command) { - config.TaskTemplate.ContainerSpec.Args = commandToArray(input.Command); - } - if (input.User) { - config.TaskTemplate.ContainerSpec.User = input.User; - } - if (input.WorkingDir) { - config.TaskTemplate.ContainerSpec.Dir = input.WorkingDir; - } - } - - function prepareEnvConfig(config, input) { - var env = []; - input.Env.forEach(function (v) { - if (v.name) { - env.push(v.name + '=' + v.value); - } - }); - config.TaskTemplate.ContainerSpec.Env = env; - } - - function prepareLabelsConfig(config, input) { - config.Labels = LabelHelper.fromKeyValueToLabelHash(input.Labels); - config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(input.ContainerLabels); - } - - function createMountObjectFromVolume(volumeObject, target, readonly) { - return { - Target: target, - Source: volumeObject.Id, - Type: 'volume', - ReadOnly: readonly, - VolumeOptions: { - Labels: volumeObject.Labels, - DriverConfig: { - Name: volumeObject.Driver, - Options: volumeObject.Options - } - } +angular.module('portainer.docker').controller('CreateServiceController', [ + '$q', + '$scope', + '$state', + '$timeout', + 'Service', + 'ServiceHelper', + 'ConfigService', + 'ConfigHelper', + 'SecretHelper', + 'SecretService', + 'VolumeService', + 'NetworkService', + 'ImageHelper', + 'LabelHelper', + 'Authentication', + 'ResourceControlService', + 'Notifications', + 'FormValidator', + 'PluginService', + 'RegistryService', + 'HttpRequestHelper', + 'NodeService', + 'SettingsService', + 'WebhookService', + 'EndpointProvider', + function ( + $q, + $scope, + $state, + $timeout, + Service, + ServiceHelper, + ConfigService, + ConfigHelper, + SecretHelper, + SecretService, + VolumeService, + NetworkService, + ImageHelper, + LabelHelper, + Authentication, + ResourceControlService, + Notifications, + FormValidator, + PluginService, + RegistryService, + HttpRequestHelper, + NodeService, + SettingsService, + WebhookService, + EndpointProvider + ) { + $scope.formValues = { + Name: '', + RegistryModel: new PorImageRegistryModel(), + Mode: 'replicated', + Replicas: 1, + Command: '', + EntryPoint: '', + WorkingDir: '', + User: '', + Env: [], + Labels: [], + ContainerLabels: [], + Volumes: [], + Network: '', + ExtraNetworks: [], + HostsEntries: [], + Ports: [], + Parallelism: 1, + PlacementConstraints: [], + PlacementPreferences: [], + UpdateDelay: '0s', + UpdateOrder: 'stop-first', + FailureAction: 'pause', + Secrets: [], + Configs: [], + AccessControlData: new AccessControlFormData(), + CpuLimit: 0, + CpuReservation: 0, + MemoryLimit: 0, + MemoryReservation: 0, + MemoryLimitUnit: 'MB', + MemoryReservationUnit: 'MB', + RestartCondition: 'any', + RestartDelay: '5s', + RestartMaxAttempts: 0, + RestartWindow: '0s', + LogDriverName: '', + LogDriverOpts: [], + Webhook: false, }; - } - function prepareVolumes(config, input) { - input.Volumes.forEach(function (volume) { - if (volume.Source && volume.Target) { - if (volume.Type !== 'volume') { - config.TaskTemplate.ContainerSpec.Mounts.push(volume); - } else { - var volumeObject = volume.Source; - var mount = createMountObjectFromVolume(volumeObject, volume.Target, volume.ReadOnly); - config.TaskTemplate.ContainerSpec.Mounts.push(mount); - } - } - }); - } + $scope.state = { + formValidationError: '', + actionInProgress: false, + }; - function prepareNetworks(config, input) { - var networks = []; - if (input.Network) { - networks.push({ Target: input.Network }); - } - input.ExtraNetworks.forEach(function (network) { - networks.push({ Target: network.Name }); - }); - config.Networks = _.uniqWith(networks, _.isEqual); - } - - function prepareHostsEntries(config, input) { - var hostsEntries = []; - if (input.HostsEntries) { - input.HostsEntries.forEach(function (host_ip) { - if (host_ip.value && host_ip.value.indexOf(':') && host_ip.value.split(':').length === 2) { - var keyVal = host_ip.value.split(':'); - // Hosts file format, IP_address canonical_hostname - hostsEntries.push(keyVal[1] + ' ' + keyVal[0]); - } + $scope.refreshSlider = function () { + $timeout(function () { + $scope.$broadcast('rzSliderForceRender'); }); - if (hostsEntries.length > 0) { - config.TaskTemplate.ContainerSpec.Hosts = hostsEntries; - } - } - } - - function prepareUpdateConfig(config, input) { - config.UpdateConfig = { - Parallelism: input.Parallelism || 0, - Delay: ServiceHelper.translateHumanDurationToNanos(input.UpdateDelay) || 0, - FailureAction: input.FailureAction, - Order: input.UpdateOrder }; - } - function prepareRestartPolicy(config, input) { - config.TaskTemplate.RestartPolicy = { - Condition: input.RestartCondition || 'any', - Delay: ServiceHelper.translateHumanDurationToNanos(input.RestartDelay) || 5000000000, - MaxAttempts: input.RestartMaxAttempts || 0, - Window: ServiceHelper.translateHumanDurationToNanos(input.RestartWindow) || 0 + $scope.addPortBinding = function () { + $scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' }); }; - } - function preparePlacementConfig(config, input) { - config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(input.PlacementConstraints); - config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(input.PlacementPreferences); - } + $scope.removePortBinding = function (index) { + $scope.formValues.Ports.splice(index, 1); + }; - function prepareConfigConfig(config, input) { - if (input.Configs) { - var configs = []; - angular.forEach(input.Configs, function(config) { - if (config.model) { - var s = ConfigHelper.configConfig(config.model); - s.File.Name = config.FileName || s.File.Name; - configs.push(s); - } - }); - config.TaskTemplate.ContainerSpec.Configs = configs; + $scope.addExtraNetwork = function () { + $scope.formValues.ExtraNetworks.push({ Name: '' }); + }; + + $scope.removeExtraNetwork = function (index) { + $scope.formValues.ExtraNetworks.splice(index, 1); + }; + + $scope.addHostsEntry = function () { + $scope.formValues.HostsEntries.push({}); + }; + + $scope.removeHostsEntry = function (index) { + $scope.formValues.HostsEntries.splice(index, 1); + }; + + $scope.addVolume = function () { + $scope.formValues.Volumes.push({ Source: '', Target: '', ReadOnly: false, Type: 'volume' }); + }; + + $scope.removeVolume = function (index) { + $scope.formValues.Volumes.splice(index, 1); + }; + + $scope.addConfig = function () { + $scope.formValues.Configs.push({}); + }; + + $scope.removeConfig = function (index) { + $scope.formValues.Configs.splice(index, 1); + }; + + $scope.addSecret = function () { + $scope.formValues.Secrets.push({ overrideTarget: false }); + }; + + $scope.removeSecret = function (index) { + $scope.formValues.Secrets.splice(index, 1); + }; + + $scope.addEnvironmentVariable = function () { + $scope.formValues.Env.push({ name: '', value: '' }); + }; + + $scope.removeEnvironmentVariable = function (index) { + $scope.formValues.Env.splice(index, 1); + }; + + $scope.addPlacementConstraint = function () { + $scope.formValues.PlacementConstraints.push({ key: '', operator: '==', value: '' }); + }; + + $scope.removePlacementConstraint = function (index) { + $scope.formValues.PlacementConstraints.splice(index, 1); + }; + + $scope.addPlacementPreference = function () { + $scope.formValues.PlacementPreferences.push({ strategy: 'spread', value: '' }); + }; + + $scope.removePlacementPreference = function (index) { + $scope.formValues.PlacementPreferences.splice(index, 1); + }; + + $scope.addLabel = function () { + $scope.formValues.Labels.push({ key: '', value: '' }); + }; + + $scope.removeLabel = function (index) { + $scope.formValues.Labels.splice(index, 1); + }; + + $scope.addContainerLabel = function () { + $scope.formValues.ContainerLabels.push({ key: '', value: '' }); + }; + + $scope.removeContainerLabel = function (index) { + $scope.formValues.ContainerLabels.splice(index, 1); + }; + + $scope.addLogDriverOpt = function () { + $scope.formValues.LogDriverOpts.push({ name: '', value: '' }); + }; + + $scope.removeLogDriverOpt = function (index) { + $scope.formValues.LogDriverOpts.splice(index, 1); + }; + + function prepareImageConfig(config, input) { + var imageConfig = ImageHelper.createImageConfigForContainer(input.RegistryModel); + config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage; } - } - function prepareSecretConfig(config, input) { - if (input.Secrets) { - var secrets = []; - angular.forEach(input.Secrets, function(secret) { - if (secret.model) { - var s = SecretHelper.secretConfig(secret.model); - s.File.Name = s.SecretName; - if (secret.overrideTarget && secret.target && secret.target !== '') { - s.File.Name = secret.target; + function preparePortsConfig(config, input) { + var ports = []; + input.Ports.forEach(function (binding) { + var port = { + Protocol: binding.Protocol, + PublishMode: binding.PublishMode, + }; + if (binding.TargetPort) { + port.TargetPort = +binding.TargetPort; + if (binding.PublishedPort) { + port.PublishedPort = +binding.PublishedPort; } - secrets.push(s); + ports.push(port); } }); - config.TaskTemplate.ContainerSpec.Secrets = secrets; + config.EndpointSpec.Ports = ports; } - } - function prepareResourcesCpuConfig(config, input) { - // CPU Limit - if (input.CpuLimit > 0) { - config.TaskTemplate.Resources.Limits.NanoCPUs = input.CpuLimit * 1000000000; + function prepareSchedulingConfig(config, input) { + if (input.Mode === 'replicated') { + config.Mode.Replicated = { + Replicas: input.Replicas, + }; + } else { + config.Mode.Global = {}; + } } - // CPU Reservation - if (input.CpuReservation > 0) { - config.TaskTemplate.Resources.Reservations.NanoCPUs = input.CpuReservation * 1000000000; - } - } - function prepareResourcesMemoryConfig(config, input) { - // Memory Limit - Round to 0.125 - var memoryLimit = (Math.round(input.MemoryLimit * 8) / 8).toFixed(3); - memoryLimit *= 1024 * 1024; - if (input.MemoryLimitUnit === 'GB') { - memoryLimit *= 1024; + function commandToArray(cmd) { + var tokens = [].concat + .apply( + [], + cmd.split("'").map(function (v, i) { + return i % 2 ? v : v.split(' '); + }) + ) + .filter(Boolean); + return tokens; } - if (memoryLimit > 0) { - config.TaskTemplate.Resources.Limits.MemoryBytes = memoryLimit; - } - // Memory Resevation - Round to 0.125 - var memoryReservation = (Math.round(input.MemoryReservation * 8) / 8).toFixed(3); - memoryReservation *= 1024 * 1024; - if (input.MemoryReservationUnit === 'GB') { - memoryReservation *= 1024; - } - if (memoryReservation > 0) { - config.TaskTemplate.Resources.Reservations.MemoryBytes = memoryReservation; - } - } - function prepareLogDriverConfig(config, input) { - var logOpts = {}; - if (input.LogDriverName) { - config.TaskTemplate.LogDriver = { Name: input.LogDriverName }; - if (input.LogDriverName !== 'none') { - input.LogDriverOpts.forEach(function (opt) { - if (opt.name) { - logOpts[opt.name] = opt.value; + function prepareCommandConfig(config, input) { + if (input.EntryPoint) { + config.TaskTemplate.ContainerSpec.Command = commandToArray(input.EntryPoint); + } + if (input.Command) { + config.TaskTemplate.ContainerSpec.Args = commandToArray(input.Command); + } + if (input.User) { + config.TaskTemplate.ContainerSpec.User = input.User; + } + if (input.WorkingDir) { + config.TaskTemplate.ContainerSpec.Dir = input.WorkingDir; + } + } + + function prepareEnvConfig(config, input) { + var env = []; + input.Env.forEach(function (v) { + if (v.name) { + env.push(v.name + '=' + v.value); + } + }); + config.TaskTemplate.ContainerSpec.Env = env; + } + + function prepareLabelsConfig(config, input) { + config.Labels = LabelHelper.fromKeyValueToLabelHash(input.Labels); + config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(input.ContainerLabels); + } + + function createMountObjectFromVolume(volumeObject, target, readonly) { + return { + Target: target, + Source: volumeObject.Id, + Type: 'volume', + ReadOnly: readonly, + VolumeOptions: { + Labels: volumeObject.Labels, + DriverConfig: { + Name: volumeObject.Driver, + Options: volumeObject.Options, + }, + }, + }; + } + + function prepareVolumes(config, input) { + input.Volumes.forEach(function (volume) { + if (volume.Source && volume.Target) { + if (volume.Type !== 'volume') { + config.TaskTemplate.ContainerSpec.Mounts.push(volume); + } else { + var volumeObject = volume.Source; + var mount = createMountObjectFromVolume(volumeObject, volume.Target, volume.ReadOnly); + config.TaskTemplate.ContainerSpec.Mounts.push(mount); + } + } + }); + } + + function prepareNetworks(config, input) { + var networks = []; + if (input.Network) { + networks.push({ Target: input.Network }); + } + input.ExtraNetworks.forEach(function (network) { + networks.push({ Target: network.Name }); + }); + config.Networks = _.uniqWith(networks, _.isEqual); + } + + function prepareHostsEntries(config, input) { + var hostsEntries = []; + if (input.HostsEntries) { + input.HostsEntries.forEach(function (host_ip) { + if (host_ip.value && host_ip.value.indexOf(':') && host_ip.value.split(':').length === 2) { + var keyVal = host_ip.value.split(':'); + // Hosts file format, IP_address canonical_hostname + hostsEntries.push(keyVal[1] + ' ' + keyVal[0]); } }); - if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) { - config.TaskTemplate.LogDriver.Options = logOpts; + if (hostsEntries.length > 0) { + config.TaskTemplate.ContainerSpec.Hosts = hostsEntries; } } } - } - function prepareConfiguration() { - var input = $scope.formValues; - var config = { - Name: input.Name, - TaskTemplate: { - ContainerSpec: { - Mounts: [] + function prepareUpdateConfig(config, input) { + config.UpdateConfig = { + Parallelism: input.Parallelism || 0, + Delay: ServiceHelper.translateHumanDurationToNanos(input.UpdateDelay) || 0, + FailureAction: input.FailureAction, + Order: input.UpdateOrder, + }; + } + + function prepareRestartPolicy(config, input) { + config.TaskTemplate.RestartPolicy = { + Condition: input.RestartCondition || 'any', + Delay: ServiceHelper.translateHumanDurationToNanos(input.RestartDelay) || 5000000000, + MaxAttempts: input.RestartMaxAttempts || 0, + Window: ServiceHelper.translateHumanDurationToNanos(input.RestartWindow) || 0, + }; + } + + function preparePlacementConfig(config, input) { + config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(input.PlacementConstraints); + config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(input.PlacementPreferences); + } + + function prepareConfigConfig(config, input) { + if (input.Configs) { + var configs = []; + angular.forEach(input.Configs, function (config) { + if (config.model) { + var s = ConfigHelper.configConfig(config.model); + s.File.Name = config.FileName || s.File.Name; + configs.push(s); + } + }); + config.TaskTemplate.ContainerSpec.Configs = configs; + } + } + + function prepareSecretConfig(config, input) { + if (input.Secrets) { + var secrets = []; + angular.forEach(input.Secrets, function (secret) { + if (secret.model) { + var s = SecretHelper.secretConfig(secret.model); + s.File.Name = s.SecretName; + if (secret.overrideTarget && secret.target && secret.target !== '') { + s.File.Name = secret.target; + } + secrets.push(s); + } + }); + config.TaskTemplate.ContainerSpec.Secrets = secrets; + } + } + + function prepareResourcesCpuConfig(config, input) { + // CPU Limit + if (input.CpuLimit > 0) { + config.TaskTemplate.Resources.Limits.NanoCPUs = input.CpuLimit * 1000000000; + } + // CPU Reservation + if (input.CpuReservation > 0) { + config.TaskTemplate.Resources.Reservations.NanoCPUs = input.CpuReservation * 1000000000; + } + } + + function prepareResourcesMemoryConfig(config, input) { + // Memory Limit - Round to 0.125 + var memoryLimit = (Math.round(input.MemoryLimit * 8) / 8).toFixed(3); + memoryLimit *= 1024 * 1024; + if (input.MemoryLimitUnit === 'GB') { + memoryLimit *= 1024; + } + if (memoryLimit > 0) { + config.TaskTemplate.Resources.Limits.MemoryBytes = memoryLimit; + } + // Memory Resevation - Round to 0.125 + var memoryReservation = (Math.round(input.MemoryReservation * 8) / 8).toFixed(3); + memoryReservation *= 1024 * 1024; + if (input.MemoryReservationUnit === 'GB') { + memoryReservation *= 1024; + } + if (memoryReservation > 0) { + config.TaskTemplate.Resources.Reservations.MemoryBytes = memoryReservation; + } + } + + function prepareLogDriverConfig(config, input) { + var logOpts = {}; + if (input.LogDriverName) { + config.TaskTemplate.LogDriver = { Name: input.LogDriverName }; + if (input.LogDriverName !== 'none') { + input.LogDriverOpts.forEach(function (opt) { + if (opt.name) { + logOpts[opt.name] = opt.value; + } + }); + if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) { + config.TaskTemplate.LogDriver.Options = logOpts; + } + } + } + } + + function prepareConfiguration() { + var input = $scope.formValues; + var config = { + Name: input.Name, + TaskTemplate: { + ContainerSpec: { + Mounts: [], + }, + Placement: {}, + Resources: { + Limits: {}, + Reservations: {}, + }, }, - Placement: {}, - Resources: { - Limits: {}, - Reservations: {} - } - }, - Mode: {}, - EndpointSpec: {} + Mode: {}, + EndpointSpec: {}, + }; + prepareSchedulingConfig(config, input); + prepareImageConfig(config, input); + preparePortsConfig(config, input); + prepareCommandConfig(config, input); + prepareEnvConfig(config, input); + prepareLabelsConfig(config, input); + prepareVolumes(config, input); + prepareNetworks(config, input); + prepareHostsEntries(config, input); + prepareUpdateConfig(config, input); + prepareConfigConfig(config, input); + prepareSecretConfig(config, input); + preparePlacementConfig(config, input); + prepareResourcesCpuConfig(config, input); + prepareResourcesMemoryConfig(config, input); + prepareRestartPolicy(config, input); + prepareLogDriverConfig(config, input); + return config; + } + + function createNewService(config, accessControlData) { + const registryModel = $scope.formValues.RegistryModel; + var authenticationDetails = registryModel.Registry.Authentication ? RegistryService.encodedCredentials(registryModel.Registry) : ''; + HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); + + Service.create(config) + .$promise.then(function success(data) { + const serviceId = data.ID; + const resourceControl = data.Portainer.ResourceControl; + const userId = Authentication.getUserDetails().ID; + const rcPromise = ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); + const webhookPromise = $q.when($scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, EndpointProvider.endpointID())); + return $q.all([rcPromise, webhookPromise]); + }) + .then(function success() { + Notifications.success('Service successfully created'); + $state.go('docker.services', {}, { reload: true }); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to create service'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + } + + function validateForm(accessControlData, isAdmin) { + $scope.state.formValidationError = ''; + var error = ''; + error = FormValidator.validateAccessControl(accessControlData, isAdmin); + + if (error) { + $scope.state.formValidationError = error; + return false; + } + return true; + } + + $scope.create = function createService() { + var accessControlData = $scope.formValues.AccessControlData; + + if (!validateForm(accessControlData, $scope.isAdmin)) { + return; + } + + $scope.state.actionInProgress = true; + var config = prepareConfiguration(); + createNewService(config, accessControlData); }; - prepareSchedulingConfig(config, input); - prepareImageConfig(config, input); - preparePortsConfig(config, input); - prepareCommandConfig(config, input); - prepareEnvConfig(config, input); - prepareLabelsConfig(config, input); - prepareVolumes(config, input); - prepareNetworks(config, input); - prepareHostsEntries(config, input); - prepareUpdateConfig(config, input); - prepareConfigConfig(config, input); - prepareSecretConfig(config, input); - preparePlacementConfig(config, input); - prepareResourcesCpuConfig(config, input); - prepareResourcesMemoryConfig(config, input); - prepareRestartPolicy(config, input); - prepareLogDriverConfig(config, input); - return config; - } - function createNewService(config, accessControlData) { - const registryModel = $scope.formValues.RegistryModel; - var authenticationDetails = registryModel.Registry.Authentication ? RegistryService.encodedCredentials(registryModel.Registry) : ''; - HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); - - Service.create(config).$promise - .then(function success(data) { - const serviceId = data.ID; - const resourceControl = data.Portainer.ResourceControl; - const userId = Authentication.getUserDetails().ID; - const rcPromise = ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); - const webhookPromise = $q.when($scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, EndpointProvider.endpointID())); - return $q.all([rcPromise, webhookPromise]); - }) - .then(function success() { - Notifications.success('Service successfully created'); - $state.go('docker.services', {}, {reload: true}); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create service'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - } - - function validateForm(accessControlData, isAdmin) { - $scope.state.formValidationError = ''; - var error = ''; - error = FormValidator.validateAccessControl(accessControlData, isAdmin); - - if (error) { - $scope.state.formValidationError = error; - return false; - } - return true; - } - - $scope.create = function createService() { - var accessControlData = $scope.formValues.AccessControlData; - - if (!validateForm(accessControlData, $scope.isAdmin)) { - return; - } - - $scope.state.actionInProgress = true; - var config = prepareConfiguration(); - createNewService(config, accessControlData); - }; - - function initSlidersMaxValuesBasedOnNodeData(nodes) { - var maxCpus = 0; - var maxMemory = 0; - for (var n in nodes) { - if (nodes[n].CPUs && nodes[n].CPUs > maxCpus) { - maxCpus = nodes[n].CPUs; + function initSlidersMaxValuesBasedOnNodeData(nodes) { + var maxCpus = 0; + var maxMemory = 0; + for (var n in nodes) { + if (nodes[n].CPUs && nodes[n].CPUs > maxCpus) { + maxCpus = nodes[n].CPUs; + } + if (nodes[n].Memory && nodes[n].Memory > maxMemory) { + maxMemory = nodes[n].Memory; + } } - if (nodes[n].Memory && nodes[n].Memory > maxMemory) { - maxMemory = nodes[n].Memory; + if (maxCpus > 0) { + $scope.state.sliderMaxCpu = maxCpus / 1000000000; + } else { + $scope.state.sliderMaxCpu = 32; + } + if (maxMemory > 0) { + $scope.state.sliderMaxMemory = Math.floor(maxMemory / 1000 / 1000); + } else { + $scope.state.sliderMaxMemory = 32768; } } - if (maxCpus > 0) { - $scope.state.sliderMaxCpu = maxCpus / 1000000000; - } else { - $scope.state.sliderMaxCpu = 32; + + function initView() { + var apiVersion = $scope.applicationState.endpoint.apiVersion; + + $q.all({ + volumes: VolumeService.volumes(), + networks: NetworkService.networks(true, true, false), + secrets: apiVersion >= 1.25 ? SecretService.secrets() : [], + configs: apiVersion >= 1.3 ? ConfigService.configs() : [], + nodes: NodeService.nodes(), + settings: SettingsService.publicSettings(), + availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25), + }) + .then(function success(data) { + $scope.availableVolumes = data.volumes; + $scope.availableNetworks = data.networks; + $scope.availableSecrets = data.secrets; + $scope.availableConfigs = data.configs; + $scope.availableLoggingDrivers = data.availableLoggingDrivers; + initSlidersMaxValuesBasedOnNodeData(data.nodes); + $scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers; + $scope.isAdmin = Authentication.isAdmin(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to initialize view'); + }); } - if (maxMemory > 0) { - $scope.state.sliderMaxMemory = Math.floor(maxMemory / 1000 / 1000); - } else { - $scope.state.sliderMaxMemory = 32768; - } - } - function initView() { - var apiVersion = $scope.applicationState.endpoint.apiVersion; - - $q.all({ - volumes: VolumeService.volumes(), - networks: NetworkService.networks(true, true, false), - secrets: apiVersion >= 1.25 ? SecretService.secrets() : [], - configs: apiVersion >= 1.30 ? ConfigService.configs() : [], - nodes: NodeService.nodes(), - settings: SettingsService.publicSettings(), - availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25) - }) - .then(function success(data) { - $scope.availableVolumes = data.volumes; - $scope.availableNetworks = data.networks; - $scope.availableSecrets = data.secrets; - $scope.availableConfigs = data.configs; - $scope.availableLoggingDrivers = data.availableLoggingDrivers; - initSlidersMaxValuesBasedOnNodeData(data.nodes); - $scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers; - $scope.isAdmin = Authentication.isAdmin(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to initialize view'); - }); - } - - initView(); -}]); + initView(); + }, +]); diff --git a/app/docker/views/services/create/createservice.html b/app/docker/views/services/create/createservice.html index 458148975..3494a88f0 100644 --- a/app/docker/views/services/create/createservice.html +++ b/app/docker/views/services/create/createservice.html @@ -1,8 +1,6 @@ - - Services > Add service - + Services > Add service
@@ -14,7 +12,7 @@
- +
@@ -22,11 +20,7 @@ Image configuration
- +
Scheduling @@ -48,7 +42,7 @@ - +
@@ -68,7 +62,7 @@
host - +
@@ -77,7 +71,7 @@
container - +
@@ -108,11 +102,12 @@
- +
@@ -125,7 +120,13 @@
- @@ -150,7 +151,7 @@
  • Labels
  • Update config & Restart
  • Secrets
  • -
  • Configs
  • +
  • Configs
  • Resources & Placement
  • @@ -165,7 +166,7 @@
    - +
    @@ -173,7 +174,7 @@
    - +
    @@ -181,11 +182,11 @@
    - +
    - +
    @@ -202,11 +203,11 @@
    name - +
    value - +

    - Logging driver for service that will override the default docker daemon driver. Select Default logging driver if you don't want to override it. Supported logging drivers can be found in the Docker documentation. + Logging driver for service that will override the default docker daemon driver. Select Default logging driver if you don't want to override it. Supported + logging drivers can be found + in the Docker documentation.

    @@ -242,9 +245,16 @@
    - + add logging driver option
    @@ -253,11 +263,11 @@
    option - +
    value - +
    - -
    @@ -292,7 +300,7 @@
    container - +
    @@ -314,7 +322,11 @@
    volume -
    @@ -322,7 +334,7 @@
    host - +
    @@ -395,7 +407,7 @@
    value - +
    -
    +
    @@ -114,11 +114,11 @@
    strategy - +
    value - +
    + Order @@ -212,28 +222,28 @@
    + {{ layer.Order }} + {{ layer.Size | humansize }}
    -
    - + {{ layer.CreatedBy | imagelayercommand }}
    diff --git a/app/docker/views/images/edit/imageController.js b/app/docker/views/images/edit/imageController.js index c13cd72ef..6a0471445 100644 --- a/app/docker/views/images/edit/imageController.js +++ b/app/docker/views/images/edit/imageController.js @@ -1,152 +1,167 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; -angular.module('portainer.docker') -.controller('ImageController', ['$q', '$scope', '$transition$', '$state', '$timeout', 'ImageService', 'ImageHelper', 'RegistryService', 'Notifications', 'HttpRequestHelper', 'ModalService', 'FileSaver', 'Blob', -function ($q, $scope, $transition$, $state, $timeout, ImageService, ImageHelper, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) { - $scope.formValues = { - RegistryModel: new PorImageRegistryModel() - }; +angular.module('portainer.docker').controller('ImageController', [ + '$q', + '$scope', + '$transition$', + '$state', + '$timeout', + 'ImageService', + 'ImageHelper', + 'RegistryService', + 'Notifications', + 'HttpRequestHelper', + 'ModalService', + 'FileSaver', + 'Blob', + function ($q, $scope, $transition$, $state, $timeout, ImageService, ImageHelper, RegistryService, Notifications, HttpRequestHelper, ModalService, FileSaver, Blob) { + $scope.formValues = { + RegistryModel: new PorImageRegistryModel(), + }; - $scope.state = { - exportInProgress: false - }; + $scope.state = { + exportInProgress: false, + }; - $scope.sortType = 'Order'; - $scope.sortReverse = false; + $scope.sortType = 'Order'; + $scope.sortReverse = false; - $scope.order = function(sortType) { - $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; - $scope.sortType = sortType; - }; + $scope.order = function (sortType) { + $scope.sortReverse = $scope.sortType === sortType ? !$scope.sortReverse : false; + $scope.sortType = sortType; + }; - $scope.toggleLayerCommand = function(layerId) { - $('#layer-command-expander'+layerId+' span').toggleClass('glyphicon-plus-sign glyphicon-minus-sign'); - $('#layer-command-'+layerId+'-short').toggle(); - $('#layer-command-'+layerId+'-full').toggle(); - }; + $scope.toggleLayerCommand = function (layerId) { + $('#layer-command-expander' + layerId + ' span').toggleClass('glyphicon-plus-sign glyphicon-minus-sign'); + $('#layer-command-' + layerId + '-short').toggle(); + $('#layer-command-' + layerId + '-full').toggle(); + }; - $scope.tagImage = function() { - const registryModel = $scope.formValues.RegistryModel; + $scope.tagImage = function () { + const registryModel = $scope.formValues.RegistryModel; - const image = ImageHelper.createImageConfigForContainer(registryModel); + const image = ImageHelper.createImageConfigForContainer(registryModel); - ImageService.tagImage($transition$.params().id, image.fromImage) - .then(function success() { - Notifications.success('Image successfully tagged'); - $state.go('docker.images.image', {id: $transition$.params().id}, {reload: true}); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to tag image'); - }); - }; + ImageService.tagImage($transition$.params().id, image.fromImage) + .then(function success() { + Notifications.success('Image successfully tagged'); + $state.go('docker.images.image', { id: $transition$.params().id }, { reload: true }); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to tag image'); + }); + }; - $scope.pushTag = function(repository) { - $('#uploadResourceHint').show(); - RegistryService.retrievePorRegistryModelFromRepository(repository) - .then(function success(registryModel) { - return ImageService.pushImage(registryModel); - }) - .then(function success() { - Notifications.success('Image successfully pushed', repository); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to push image to repository'); - }) - .finally(function final() { - $('#uploadResourceHint').hide(); - }); - }; + $scope.pushTag = function (repository) { + $('#uploadResourceHint').show(); + RegistryService.retrievePorRegistryModelFromRepository(repository) + .then(function success(registryModel) { + return ImageService.pushImage(registryModel); + }) + .then(function success() { + Notifications.success('Image successfully pushed', repository); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to push image to repository'); + }) + .finally(function final() { + $('#uploadResourceHint').hide(); + }); + }; - $scope.pullTag = function(repository) { - $('#downloadResourceHint').show(); - RegistryService.retrievePorRegistryModelFromRepository(repository) - .then(function success(registryModel) { - return ImageService.pullImage(registryModel, false); - }) - .then(function success() { - Notifications.success('Image successfully pulled', repository); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to pull image'); - }) - .finally(function final() { - $('#downloadResourceHint').hide(); - }); - }; + $scope.pullTag = function (repository) { + $('#downloadResourceHint').show(); + RegistryService.retrievePorRegistryModelFromRepository(repository) + .then(function success(registryModel) { + return ImageService.pullImage(registryModel, false); + }) + .then(function success() { + Notifications.success('Image successfully pulled', repository); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to pull image'); + }) + .finally(function final() { + $('#downloadResourceHint').hide(); + }); + }; - $scope.removeTag = function(repository) { - ImageService.deleteImage(repository, false) - .then(function success() { - if ($scope.image.RepoTags.length === 1) { - Notifications.success('Image successfully deleted', repository); - $state.go('docker.images', {}, {reload: true}); - } else { - Notifications.success('Tag successfully deleted', repository); - $state.go('docker.images.image', {id: $transition$.params().id}, {reload: true}); - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove image'); - }); - }; + $scope.removeTag = function (repository) { + ImageService.deleteImage(repository, false) + .then(function success() { + if ($scope.image.RepoTags.length === 1) { + Notifications.success('Image successfully deleted', repository); + $state.go('docker.images', {}, { reload: true }); + } else { + Notifications.success('Tag successfully deleted', repository); + $state.go('docker.images.image', { id: $transition$.params().id }, { reload: true }); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove image'); + }); + }; - $scope.removeImage = function (id) { - ImageService.deleteImage(id, false) - .then(function success() { - Notifications.success('Image successfully deleted', id); - $state.go('docker.images', {}, {reload: true}); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove image'); - }); - }; + $scope.removeImage = function (id) { + ImageService.deleteImage(id, false) + .then(function success() { + Notifications.success('Image successfully deleted', id); + $state.go('docker.images', {}, { reload: true }); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove image'); + }); + }; - function exportImage(image) { - HttpRequestHelper.setPortainerAgentTargetHeader(image.NodeName); - $scope.state.exportInProgress = true; - ImageService.downloadImages([image]) - .then(function success(data) { - var downloadData = new Blob([data.file], { type: 'application/x-tar' }); - FileSaver.saveAs(downloadData, 'images.tar'); - Notifications.success('Image successfully downloaded'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to download image'); - }) - .finally(function final() { - $scope.state.exportInProgress = false; - }); - } + function exportImage(image) { + HttpRequestHelper.setPortainerAgentTargetHeader(image.NodeName); + $scope.state.exportInProgress = true; + ImageService.downloadImages([image]) + .then(function success(data) { + var downloadData = new Blob([data.file], { type: 'application/x-tar' }); + FileSaver.saveAs(downloadData, 'images.tar'); + Notifications.success('Image successfully downloaded'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to download image'); + }) + .finally(function final() { + $scope.state.exportInProgress = false; + }); + } - $scope.exportImage = function (image) { - if (image.RepoTags.length === 0 || _.includes(image.RepoTags, '')) { - Notifications.warning('', 'Cannot download a untagged image'); - return; - } + $scope.exportImage = function (image) { + if (image.RepoTags.length === 0 || _.includes(image.RepoTags, '')) { + Notifications.warning('', 'Cannot download a untagged image'); + return; + } - ModalService.confirmImageExport(function (confirmed) { - if(!confirmed) { return; } - exportImage(image); - }); - }; + ModalService.confirmImageExport(function (confirmed) { + if (!confirmed) { + return; + } + exportImage(image); + }); + }; - function initView() { - HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); - var endpointProvider = $scope.applicationState.endpoint.mode.provider; - $q.all({ - image: ImageService.image($transition$.params().id), - history: endpointProvider !== 'VMWARE_VIC' ? ImageService.history($transition$.params().id) : [] - }) - .then(function success(data) { - $scope.image = data.image; - $scope.history = data.history; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve image details'); - $state.go('docker.images'); - }); - } + function initView() { + HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); + var endpointProvider = $scope.applicationState.endpoint.mode.provider; + $q.all({ + image: ImageService.image($transition$.params().id), + history: endpointProvider !== 'VMWARE_VIC' ? ImageService.history($transition$.params().id) : [], + }) + .then(function success(data) { + $scope.image = data.image; + $scope.history = data.history; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve image details'); + $state.go('docker.images'); + }); + } - initView(); -}]); + initView(); + }, +]); diff --git a/app/docker/views/images/images.html b/app/docker/views/images/images.html index d8d82e043..9c7cb36b9 100644 --- a/app/docker/views/images/images.html +++ b/app/docker/views/images/images.html @@ -10,31 +10,29 @@
    - - + - +
    Deployment
    - - +
    - @@ -49,16 +47,18 @@
    diff --git a/app/docker/views/images/imagesController.js b/app/docker/views/images/imagesController.js index aed6f8cf6..1933af39f 100644 --- a/app/docker/views/images/imagesController.js +++ b/app/docker/views/images/imagesController.js @@ -1,137 +1,149 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; -angular.module('portainer.docker') -.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'ModalService', 'HttpRequestHelper', 'FileSaver', 'Blob', 'EndpointProvider', -function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob, EndpointProvider) { - $scope.state = { - actionInProgress: false, - exportInProgress: false - }; +angular.module('portainer.docker').controller('ImagesController', [ + '$scope', + '$state', + 'ImageService', + 'Notifications', + 'ModalService', + 'HttpRequestHelper', + 'FileSaver', + 'Blob', + 'EndpointProvider', + function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob, EndpointProvider) { + $scope.state = { + actionInProgress: false, + exportInProgress: false, + }; - $scope.formValues = { - RegistryModel: new PorImageRegistryModel(), - NodeName: null - }; + $scope.formValues = { + RegistryModel: new PorImageRegistryModel(), + NodeName: null, + }; - $scope.pullImage = function() { - const registryModel = $scope.formValues.RegistryModel; + $scope.pullImage = function () { + const registryModel = $scope.formValues.RegistryModel; - var nodeName = $scope.formValues.NodeName; - HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); + var nodeName = $scope.formValues.NodeName; + HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); - $scope.state.actionInProgress = true; - ImageService.pullImage(registryModel, false) - .then(function success() { - Notifications.success('Image successfully pulled', registryModel.Image); - $state.reload(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to pull image'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; + $scope.state.actionInProgress = true; + ImageService.pullImage(registryModel, false) + .then(function success() { + Notifications.success('Image successfully pulled', registryModel.Image); + $state.reload(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to pull image'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + }; - $scope.confirmRemovalAction = function (selectedItems, force) { - ModalService.confirmImageForceRemoval(function (confirmed) { - if(!confirmed) { return; } - $scope.removeAction(selectedItems, force); - }); - }; - - function isAuthorizedToDownload(selectedItems) { - - for (var i = 0; i < selectedItems.length; i++) { - var image = selectedItems[i]; - - var untagged = _.find(image.RepoTags, function (item) { - return item.indexOf('') > -1; + $scope.confirmRemovalAction = function (selectedItems, force) { + ModalService.confirmImageForceRemoval(function (confirmed) { + if (!confirmed) { + return; + } + $scope.removeAction(selectedItems, force); }); + }; - if (untagged) { - Notifications.warning('', 'Cannot download a untagged image'); + function isAuthorizedToDownload(selectedItems) { + for (var i = 0; i < selectedItems.length; i++) { + var image = selectedItems[i]; + + var untagged = _.find(image.RepoTags, function (item) { + return item.indexOf('') > -1; + }); + + if (untagged) { + Notifications.warning('', 'Cannot download a untagged image'); + return false; + } + } + + if (_.uniqBy(selectedItems, 'NodeName').length > 1) { + Notifications.warning('', 'Cannot download images from different nodes at the same time'); return false; } + + return true; } - if (_.uniqBy(selectedItems, 'NodeName').length > 1) { - Notifications.warning('', 'Cannot download images from different nodes at the same time'); - return false; + function exportImages(images) { + HttpRequestHelper.setPortainerAgentTargetHeader(images[0].NodeName); + $scope.state.exportInProgress = true; + ImageService.downloadImages(images) + .then(function success(data) { + var downloadData = new Blob([data.file], { type: 'application/x-tar' }); + FileSaver.saveAs(downloadData, 'images.tar'); + Notifications.success('Image(s) successfully downloaded'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to download image(s)'); + }) + .finally(function final() { + $scope.state.exportInProgress = false; + }); } - return true; - } + $scope.downloadAction = function (selectedItems) { + if (!isAuthorizedToDownload(selectedItems)) { + return; + } - function exportImages(images) { - HttpRequestHelper.setPortainerAgentTargetHeader(images[0].NodeName); - $scope.state.exportInProgress = true; - ImageService.downloadImages(images) - .then(function success(data) { - var downloadData = new Blob([data.file], { type: 'application/x-tar' }); - FileSaver.saveAs(downloadData, 'images.tar'); - Notifications.success('Image(s) successfully downloaded'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to download image(s)'); - }) - .finally(function final() { - $scope.state.exportInProgress = false; - }); - } - - $scope.downloadAction = function (selectedItems) { - if (!isAuthorizedToDownload(selectedItems)) { - return; - } - - ModalService.confirmImageExport(function (confirmed) { - if(!confirmed) { return; } - exportImages(selectedItems); - }); - }; - - $scope.removeAction = function (selectedItems, force) { - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (image) { - HttpRequestHelper.setPortainerAgentTargetHeader(image.NodeName); - ImageService.deleteImage(image.Id, force) - .then(function success() { - Notifications.success('Image successfully removed', image.Id); - var index = $scope.images.indexOf(image); - $scope.images.splice(index, 1); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove image'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); + ModalService.confirmImageExport(function (confirmed) { + if (!confirmed) { + return; } + exportImages(selectedItems); }); - }); - }; + }; - $scope.offlineMode = false; + $scope.removeAction = function (selectedItems, force) { + var actionCount = selectedItems.length; + angular.forEach(selectedItems, function (image) { + HttpRequestHelper.setPortainerAgentTargetHeader(image.NodeName); + ImageService.deleteImage(image.Id, force) + .then(function success() { + Notifications.success('Image successfully removed', image.Id); + var index = $scope.images.indexOf(image); + $scope.images.splice(index, 1); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove image'); + }) + .finally(function final() { + --actionCount; + if (actionCount === 0) { + $state.reload(); + } + }); + }); + }; - $scope.getImages = getImages; - function getImages() { - ImageService.images(true) - .then(function success(data) { - $scope.images = data; - $scope.offlineMode = EndpointProvider.offlineMode(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve images'); - $scope.images = []; - }); - } + $scope.offlineMode = false; - function initView() { - getImages(); - } + $scope.getImages = getImages; + function getImages() { + ImageService.images(true) + .then(function success(data) { + $scope.images = data; + $scope.offlineMode = EndpointProvider.offlineMode(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve images'); + $scope.images = []; + }); + } - initView(); -}]); + function initView() { + getImages(); + } + + initView(); + }, +]); diff --git a/app/docker/views/images/import/importImageController.js b/app/docker/views/images/import/importImageController.js index b993a614f..d8d7d0027 100644 --- a/app/docker/views/images/import/importImageController.js +++ b/app/docker/views/images/import/importImageController.js @@ -1,31 +1,35 @@ -angular.module('portainer.docker') -.controller('ImportImageController', ['$scope', '$state', 'ImageService', 'Notifications', 'HttpRequestHelper', -function ($scope, $state, ImageService, Notifications, HttpRequestHelper) { +angular.module('portainer.docker').controller('ImportImageController', [ + '$scope', + '$state', + 'ImageService', + 'Notifications', + 'HttpRequestHelper', + function ($scope, $state, ImageService, Notifications, HttpRequestHelper) { + $scope.state = { + actionInProgress: false, + }; - $scope.state = { - actionInProgress: false - }; + $scope.formValues = { + UploadFile: null, + NodeName: null, + }; - $scope.formValues = { - UploadFile: null, - NodeName: null - }; + $scope.uploadImage = function () { + $scope.state.actionInProgress = true; - $scope.uploadImage = function() { - $scope.state.actionInProgress = true; - - var nodeName = $scope.formValues.NodeName; - HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); - var file = $scope.formValues.UploadFile; - ImageService.uploadImage(file) - .then(function success() { - Notifications.success('Images successfully uploaded'); - }) - .catch(function error(err) { - Notifications.error('Failure', err.message, 'Unable to upload image'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; -}]); + var nodeName = $scope.formValues.NodeName; + HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); + var file = $scope.formValues.UploadFile; + ImageService.uploadImage(file) + .then(function success() { + Notifications.success('Images successfully uploaded'); + }) + .catch(function error(err) { + Notifications.error('Failure', err.message, 'Unable to upload image'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + }; + }, +]); diff --git a/app/docker/views/images/import/importimage.html b/app/docker/views/images/import/importimage.html index 6a6b20f31..5f10e4562 100644 --- a/app/docker/views/images/import/importimage.html +++ b/app/docker/views/images/import/importimage.html @@ -1,8 +1,6 @@ - - Images > Import image - + Images > Import image
    @@ -21,7 +19,9 @@
    - + {{ formValues.UploadFile.name }} @@ -34,8 +34,7 @@ Deployment
    - - +
    @@ -44,8 +43,13 @@
    - @@ -57,4 +61,4 @@
    -
    \ No newline at end of file +
    diff --git a/app/docker/views/networks/create/createNetworkController.js b/app/docker/views/networks/create/createNetworkController.js index a592582a6..233c221f1 100644 --- a/app/docker/views/networks/create/createNetworkController.js +++ b/app/docker/views/networks/create/createNetworkController.js @@ -1,218 +1,235 @@ import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel'; import { MacvlanFormData } from '../../../components/network-macvlan-form/networkMacvlanFormModel'; -angular.module('portainer.docker') - .controller('CreateNetworkController', ['$q', '$scope', '$state', 'PluginService', 'Notifications', 'NetworkService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator', 'HttpRequestHelper', - function ($q, $scope, $state, PluginService, Notifications, NetworkService, LabelHelper, Authentication, ResourceControlService, FormValidator, HttpRequestHelper) { +angular.module('portainer.docker').controller('CreateNetworkController', [ + '$q', + '$scope', + '$state', + 'PluginService', + 'Notifications', + 'NetworkService', + 'LabelHelper', + 'Authentication', + 'ResourceControlService', + 'FormValidator', + 'HttpRequestHelper', + function ($q, $scope, $state, PluginService, Notifications, NetworkService, LabelHelper, Authentication, ResourceControlService, FormValidator, HttpRequestHelper) { + $scope.formValues = { + DriverOptions: [], + Subnet: '', + Gateway: '', + IPRange: '', + AuxAddress: '', + Labels: [], + AccessControlData: new AccessControlFormData(), + NodeName: null, + Macvlan: new MacvlanFormData(), + }; - $scope.formValues = { - DriverOptions: [], - Subnet: '', - Gateway: '', - IPRange: '', - AuxAddress: '', - Labels: [], - AccessControlData: new AccessControlFormData(), - NodeName: null, - Macvlan: new MacvlanFormData() - }; + $scope.state = { + formValidationError: '', + actionInProgress: false, + }; - $scope.state = { - formValidationError: '', - actionInProgress: false - }; + $scope.availableNetworkDrivers = []; - $scope.availableNetworkDrivers = []; + $scope.config = { + Driver: 'bridge', + CheckDuplicate: true, + Internal: false, + Attachable: false, + // Force IPAM Driver to 'default', should not be required. + // See: https://github.com/docker/docker/issues/25735 + IPAM: { + Driver: 'default', + Config: [], + }, + Labels: {}, + }; - $scope.config = { - Driver: 'bridge', - CheckDuplicate: true, - Internal: false, - Attachable: false, - // Force IPAM Driver to 'default', should not be required. - // See: https://github.com/docker/docker/issues/25735 - IPAM: { - Driver: 'default', - Config: [] - }, - Labels: {} - }; + $scope.addDriverOption = function () { + $scope.formValues.DriverOptions.push({ + name: '', + value: '', + }); + }; - $scope.addDriverOption = function () { - $scope.formValues.DriverOptions.push({ - name: '', - value: '' - }); - }; + $scope.removeDriverOption = function (index) { + $scope.formValues.DriverOptions.splice(index, 1); + }; - $scope.removeDriverOption = function (index) { - $scope.formValues.DriverOptions.splice(index, 1); - }; + $scope.addLabel = function () { + $scope.formValues.Labels.push({ + key: '', + value: '', + }); + }; - $scope.addLabel = function () { - $scope.formValues.Labels.push({ - key: '', - value: '' - }); - }; + $scope.removeLabel = function (index) { + $scope.formValues.Labels.splice(index, 1); + }; - $scope.removeLabel = function (index) { - $scope.formValues.Labels.splice(index, 1); - }; - - function prepareIPAMConfiguration(config) { - if ($scope.formValues.Subnet) { - var ipamConfig = {}; - ipamConfig.Subnet = $scope.formValues.Subnet; - if ($scope.formValues.Gateway) { - ipamConfig.Gateway = $scope.formValues.Gateway; - } - if ($scope.formValues.IPRange) { - ipamConfig.IPRange = $scope.formValues.IPRange; - } - if ($scope.formValues.AuxAddress) { - ipamConfig.AuxAddress = $scope.formValues.AuxAddress; - } - config.IPAM.Config.push(ipamConfig); + function prepareIPAMConfiguration(config) { + if ($scope.formValues.Subnet) { + var ipamConfig = {}; + ipamConfig.Subnet = $scope.formValues.Subnet; + if ($scope.formValues.Gateway) { + ipamConfig.Gateway = $scope.formValues.Gateway; } - } - - function prepareDriverOptions(config) { - var options = {}; - $scope.formValues.DriverOptions.forEach(function (option) { - options[option.name] = option.value; - }); - config.Options = options; - } - - function prepareLabelsConfig(config) { - config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels); - } - - function prepareConfiguration() { - var config = angular.copy($scope.config); - prepareIPAMConfiguration(config); - prepareDriverOptions(config); - prepareLabelsConfig(config); - return config; - } - - function modifyNetworkConfigurationForMacvlanConfigOnly(config) { - config.Internal = null; - config.Attachable = null; - config.ConfigOnly = true; - config.Options.parent = $scope.formValues.Macvlan.ParentNetworkCard; - } - - function modifyNetworkConfigurationForMacvlanConfigFrom(config, selectedNetworkConfig) { - config.ConfigFrom = { - Network: selectedNetworkConfig.Name - }; - if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { - config.Scope = 'swarm'; - } else { - config.Scope = 'local'; + if ($scope.formValues.IPRange) { + ipamConfig.IPRange = $scope.formValues.IPRange; } + if ($scope.formValues.AuxAddress) { + ipamConfig.AuxAddress = $scope.formValues.AuxAddress; + } + config.IPAM.Config.push(ipamConfig); } - - function validateForm(accessControlData, isAdmin) { - $scope.state.formValidationError = ''; - var error = ''; - error = FormValidator.validateAccessControl(accessControlData, isAdmin); - - if (error) { - $scope.state.formValidationError = error; - return false; - } - return true; - } - - function createNetwork(context) { - HttpRequestHelper.setPortainerAgentTargetHeader(context.nodeName); - HttpRequestHelper.setPortainerAgentManagerOperation(context.managerOperation); - - $scope.state.actionInProgress = true; - NetworkService.create(context.networkConfiguration) - .then(function success(data) { - const userId = context.userDetails.ID; - const accessControlData = context.accessControlData; - const resourceControl = data.Portainer.ResourceControl; - return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); - }) - .then(function success() { - Notifications.success('Network successfully created'); - if (context.reload) { - $state.go('docker.networks', {}, { - reload: true - }); - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'An error occured during network creation'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - } - - $scope.create = function () { - var networkConfiguration = prepareConfiguration(); - var accessControlData = $scope.formValues.AccessControlData; - var userDetails = Authentication.getUserDetails(); - var isAdmin = Authentication.isAdmin(); - - if (!validateForm(accessControlData, isAdmin)) { - return; - } - - var creationContext = { - nodeName: $scope.formValues.NodeName, - managerOperation: false, - networkConfiguration: networkConfiguration, - userDetails: userDetails, - accessControlData: accessControlData, - reload: true - }; - - if ($scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && $scope.config.Driver === 'overlay') { - creationContext.managerOperation = true; - } - - if ($scope.config.Driver === 'macvlan') { - if ($scope.formValues.Macvlan.Scope === 'local') { - modifyNetworkConfigurationForMacvlanConfigOnly(networkConfiguration); - } else if ($scope.formValues.Macvlan.Scope === 'swarm') { - var selectedNetworkConfig = $scope.formValues.Macvlan.SelectedNetworkConfig; - modifyNetworkConfigurationForMacvlanConfigFrom(networkConfiguration, selectedNetworkConfig); - creationContext.nodeName = selectedNetworkConfig.NodeName; - } - } - - if ($scope.config.Driver === 'macvlan' && $scope.formValues.Macvlan.Scope === 'local' && - $scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { - var selectedNodes = $scope.formValues.Macvlan.DatatableState.selectedItems; - selectedNodes.forEach(function (node, idx) { - creationContext.nodeName = node.Hostname; - creationContext.reload = idx === selectedNodes.length - 1 ? true : false; - createNetwork(creationContext); - }); - } else { - createNetwork(creationContext); - } - }; - - function initView() { - var apiVersion = $scope.applicationState.endpoint.apiVersion; - - PluginService.networkPlugins(apiVersion < 1.25) - .then(function success(data) { - $scope.availableNetworkDrivers = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve network drivers'); - }); - } - - initView(); } - ]); + + function prepareDriverOptions(config) { + var options = {}; + $scope.formValues.DriverOptions.forEach(function (option) { + options[option.name] = option.value; + }); + config.Options = options; + } + + function prepareLabelsConfig(config) { + config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels); + } + + function prepareConfiguration() { + var config = angular.copy($scope.config); + prepareIPAMConfiguration(config); + prepareDriverOptions(config); + prepareLabelsConfig(config); + return config; + } + + function modifyNetworkConfigurationForMacvlanConfigOnly(config) { + config.Internal = null; + config.Attachable = null; + config.ConfigOnly = true; + config.Options.parent = $scope.formValues.Macvlan.ParentNetworkCard; + } + + function modifyNetworkConfigurationForMacvlanConfigFrom(config, selectedNetworkConfig) { + config.ConfigFrom = { + Network: selectedNetworkConfig.Name, + }; + if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { + config.Scope = 'swarm'; + } else { + config.Scope = 'local'; + } + } + + function validateForm(accessControlData, isAdmin) { + $scope.state.formValidationError = ''; + var error = ''; + error = FormValidator.validateAccessControl(accessControlData, isAdmin); + + if (error) { + $scope.state.formValidationError = error; + return false; + } + return true; + } + + function createNetwork(context) { + HttpRequestHelper.setPortainerAgentTargetHeader(context.nodeName); + HttpRequestHelper.setPortainerAgentManagerOperation(context.managerOperation); + + $scope.state.actionInProgress = true; + NetworkService.create(context.networkConfiguration) + .then(function success(data) { + const userId = context.userDetails.ID; + const accessControlData = context.accessControlData; + const resourceControl = data.Portainer.ResourceControl; + return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); + }) + .then(function success() { + Notifications.success('Network successfully created'); + if (context.reload) { + $state.go( + 'docker.networks', + {}, + { + reload: true, + } + ); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'An error occured during network creation'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + } + + $scope.create = function () { + var networkConfiguration = prepareConfiguration(); + var accessControlData = $scope.formValues.AccessControlData; + var userDetails = Authentication.getUserDetails(); + var isAdmin = Authentication.isAdmin(); + + if (!validateForm(accessControlData, isAdmin)) { + return; + } + + var creationContext = { + nodeName: $scope.formValues.NodeName, + managerOperation: false, + networkConfiguration: networkConfiguration, + userDetails: userDetails, + accessControlData: accessControlData, + reload: true, + }; + + if ($scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && $scope.config.Driver === 'overlay') { + creationContext.managerOperation = true; + } + + if ($scope.config.Driver === 'macvlan') { + if ($scope.formValues.Macvlan.Scope === 'local') { + modifyNetworkConfigurationForMacvlanConfigOnly(networkConfiguration); + } else if ($scope.formValues.Macvlan.Scope === 'swarm') { + var selectedNetworkConfig = $scope.formValues.Macvlan.SelectedNetworkConfig; + modifyNetworkConfigurationForMacvlanConfigFrom(networkConfiguration, selectedNetworkConfig); + creationContext.nodeName = selectedNetworkConfig.NodeName; + } + } + + if ( + $scope.config.Driver === 'macvlan' && + $scope.formValues.Macvlan.Scope === 'local' && + $scope.applicationState.endpoint.mode.agentProxy && + $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' + ) { + var selectedNodes = $scope.formValues.Macvlan.DatatableState.selectedItems; + selectedNodes.forEach(function (node, idx) { + creationContext.nodeName = node.Hostname; + creationContext.reload = idx === selectedNodes.length - 1 ? true : false; + createNetwork(creationContext); + }); + } else { + createNetwork(creationContext); + } + }; + + function initView() { + var apiVersion = $scope.applicationState.endpoint.apiVersion; + + PluginService.networkPlugins(apiVersion < 1.25) + .then(function success(data) { + $scope.availableNetworkDrivers = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve network drivers'); + }); + } + + initView(); + }, +]); diff --git a/app/docker/views/networks/create/createnetwork.html b/app/docker/views/networks/create/createnetwork.html index 03711c36c..fedcad7bc 100644 --- a/app/docker/views/networks/create/createnetwork.html +++ b/app/docker/views/networks/create/createnetwork.html @@ -1,8 +1,6 @@ - - Networks > Add network - + Networks > Add network
    @@ -14,7 +12,7 @@
    - +
    @@ -28,7 +26,7 @@ - +
    @@ -37,7 +35,10 @@
    add driver option @@ -48,11 +49,11 @@
    name - +
    value - +
    @@ -144,19 +143,25 @@ Enable manual container attachment
    -
    +
    Deployment
    - - +
    @@ -168,8 +173,13 @@
    - @@ -182,4 +192,4 @@
    -
    \ No newline at end of file + diff --git a/app/docker/views/networks/edit/network.html b/app/docker/views/networks/edit/network.html index b5ee95ce0..b0be069f4 100644 --- a/app/docker/views/networks/edit/network.html +++ b/app/docker/views/networks/edit/network.html @@ -20,7 +20,9 @@
    ID {{ network.Id }} - +
    {{ container.Name }}{{ container.Name }} {{ container.IPv4Address || '-' }} {{ container.IPv6Address || '-' }} {{ container.MacAddress || '-' }} - +
    ID {{ secret.Id }} - +
    @@ -25,9 +22,18 @@ - + diff --git a/app/docker/views/services/edit/includes/constraints.html b/app/docker/views/services/edit/includes/constraints.html index 69c681e4e..372bd6660 100644 --- a/app/docker/views/services/edit/includes/constraints.html +++ b/app/docker/views/services/edit/includes/constraints.html @@ -11,7 +11,7 @@

    There are no placement constraints for this service.

    -
    {{ config.Name }}{{ config.Name }} - + {{ config.Uid }} {{ config.Gid }}
    +
    @@ -23,12 +23,27 @@ - + - +
    Name
    - +
    - @@ -36,7 +51,15 @@
    - +
    CMD{{ service.Command|command }}{{ service.Command | command }}

    Command to execute. @@ -15,7 +17,9 @@

    Args{{ service.Arguments }}{{ service.Arguments }}

    Arguments passed to command in container. diff --git a/app/docker/views/services/edit/includes/containerlabels.html b/app/docker/views/services/edit/includes/containerlabels.html index 0ebac64b2..6a75a84d1 100644 --- a/app/docker/views/services/edit/includes/containerlabels.html +++ b/app/docker/views/services/edit/includes/containerlabels.html @@ -11,7 +11,7 @@

    There are no container labels for this service.

    - +
    @@ -23,13 +23,29 @@
    Label
    name - +
    value - + + diff --git a/app/docker/views/services/edit/includes/environmentvariables.html b/app/docker/views/services/edit/includes/environmentvariables.html index aa9298057..32e4d425f 100644 --- a/app/docker/views/services/edit/includes/environmentvariables.html +++ b/app/docker/views/services/edit/includes/environmentvariables.html @@ -11,7 +11,7 @@

    There are no environment variables for this service.

    - +
    @@ -23,13 +23,21 @@
    Name
    name - +
    value - +