11from pathlib import Path
2+ from dataclasses import dataclass
23from copy import deepcopy
34from collections import defaultdict
45from typing import Dict , Any , List , NamedTuple , DefaultDict , Optional
5- import requests as rq
6+ import requests
7+ from requests .adapters import HTTPAdapter
8+ from requests .packages .urllib3 .util import Retry
69from schema_entry import EntryPoint
710from pyloggerhelper import log
811from .utils import (
9- GitStackInfo ,
12+ HttpCodeError ,
1013 base_schema_properties ,
11- get_jwt ,
12- get_swarm_id ,
13- get_all_stacks_from_portainer ,
14- NotSwarmEndpointError
14+ get_jwt
1515)
1616
1717
7575 "type" : "string" ,
7676 "description" : "执行位置" ,
7777 "default" : "."
78- },
79- "retry_max_times" : {
80- "type" : "integer" ,
81- "description" : "重试次数" ,
82- },
83- "retry_interval" : {
84- "type" : "number" ,
85- "description" : "重试间隔时间" ,
86- "default" : 3
8778 }
8879}
8980
9081schema_properties .update (** base_schema_properties )
9182
9283
84+ class NotSwarmEndpointError (Exception ):
85+ """节点不是swarm节点"""
86+ pass
87+
88+
9389class ComposeInfo (NamedTuple ):
9490 stack_name : str
9591 path : str
9692
9793
94+ @dataclass
95+ class GitStackInfo :
96+ endpoint_id : int
97+ stack_name : str
98+ stack_id : Optional [int ] = None
99+ swarm_id : Optional [str ] = None
100+ composeFilePathInRepository : Optional [str ] = None
101+ repositoryReferenceName : Optional [str ] = None
102+ repositoryURL : Optional [str ] = None
103+ env : Optional [List [Dict [str , str ]]] = None
104+
105+ def check_git_repository (self ) -> None :
106+ if not (self .composeFilePathInRepository and self .repositoryReferenceName and self .repositoryURL ):
107+ raise AttributeError ("need git repository info" )
108+
109+ def update_to_portainer (self , rq : requests .Session , base_url : str , jwt : str , prune : bool = False ,
110+ repositoryUsername : Optional [str ] = None , repositoryPassword : Optional [str ] = None ) -> None :
111+ """更新portainer中的stack
112+
113+ Args:
114+ base_url (str): portainer的根地址
115+ jwt (str): 访问jwt
116+ prune (bool, optional): 更新是否删除不再使用的资源. Defaults to False.
117+ repositoryUsername (Optional[str], optional): git仓库账户. Defaults to None.
118+ repositoryPassword (Optional[str], optional): git仓库密码. Defaults to None.
119+
120+ Raises:
121+ HttpCodeError: update stack query get error
122+ e: update stack query get json result error
123+ """
124+ body : Dict [str , Any ] = {
125+ "env" : self .env ,
126+ "prune" : prune ,
127+ "repositoryReferenceName" : self .repositoryReferenceName ,
128+ }
129+ if repositoryUsername and repositoryPassword :
130+ repositoryAuthentication = True
131+ body .update ({
132+ "repositoryAuthentication" : repositoryAuthentication ,
133+ "repositoryPassword" : repositoryPassword ,
134+ "repositoryUsername" : repositoryUsername
135+ })
136+ else :
137+ repositoryAuthentication = False
138+ body .update ({
139+ "repositoryAuthentication" : repositoryAuthentication
140+ })
141+ res = rq .put (
142+ f"{ base_url } /api/stacks/{ self .stack_id } /git" ,
143+ headers = requests .structures .CaseInsensitiveDict ({"Authorization" : "Bearer " + jwt }),
144+ params = {
145+ "endpointId" : self .endpoint_id
146+ },
147+ json = body )
148+ if res .status_code != 200 :
149+ log .error ("update stack query get error" ,
150+ base_url = base_url ,
151+ status_code = res .status_code ,
152+ stack = self )
153+ raise HttpCodeError ("update stack query get error" )
154+ try :
155+ res_json = res .json ()
156+ except Exception as e :
157+ log .error ("update stack query get json result error" , stack = self , err = type (e ), err_msg = str (e ), exc_info = True , stack_info = True )
158+ raise e
159+ else :
160+ log .info ("update stack ok" , stack = self , result = res_json )
161+
162+ def create_to_portainer (self , rq : requests .Session , base_url : str , jwt : str ,
163+ repositoryUsername : Optional [str ] = None , repositoryPassword : Optional [str ] = None ) -> None :
164+ """使用对象的信息创建stack.
165+
166+ Args:
167+ base_url (str): portainer的根地址
168+ jwt (str): 访问jwt
169+ repositoryUsername (Optional[str], optional): git仓库账户. Defaults to None.
170+ repositoryPassword (Optional[str], optional): git仓库密码. Defaults to None.
171+
172+ Raises:
173+ AttributeError: only create git repository method
174+ HttpCodeError: create stack query get error
175+ e: create stack query get json result error
176+ """
177+ body : Dict [str , Any ] = {
178+ "env" : self .env ,
179+ "composeFilePathInRepository" : self .composeFilePathInRepository ,
180+ "name" : self .stack_name ,
181+ "repositoryReferenceName" : self .repositoryReferenceName ,
182+ "repositoryURL" : self .repositoryURL ,
183+ }
184+ if repositoryUsername and repositoryPassword :
185+ repositoryAuthentication = True
186+ body .update ({
187+ "repositoryAuthentication" : repositoryAuthentication ,
188+ "repositoryPassword" : repositoryPassword ,
189+ "repositoryUsername" : repositoryUsername
190+ })
191+ else :
192+ repositoryAuthentication = False
193+ body .update ({
194+ "repositoryAuthentication" : repositoryAuthentication
195+ })
196+ if self .swarm_id :
197+ stack_type = 1
198+ body .update ({"swarmID" : self .swarm_id })
199+ else :
200+ stack_type = 2
201+ params = (("method" , "repository" ), ("type" , stack_type ), ("endpointId" , self .endpoint_id ))
202+ res = rq .post (
203+ f"{ base_url } /api/stacks" ,
204+ headers = requests .structures .CaseInsensitiveDict ({"Authorization" : "Bearer " + jwt }),
205+ params = params ,
206+ json = body )
207+ if res .status_code != 200 :
208+ log .error ("create stack query get error" ,
209+ base_url = base_url ,
210+ status_code = res .status_code ,
211+ stack = self )
212+ raise HttpCodeError ("create stack query get error" )
213+ try :
214+ res_json = res .json ()
215+ except Exception as e :
216+ log .error ("create stack query get json result error" , stack = self , err = type (e ), err_msg = str (e ), exc_info = True , stack_info = True )
217+ raise e
218+ else :
219+ log .info ("create stack ok" , stack = self , result = res_json )
220+
221+ def update_or_create (self , rq : requests .Session , base_url : str , jwt : str , prune : bool = False ,
222+ repositoryUsername : Optional [str ] = None , repositoryPassword : Optional [str ] = None ) -> None :
223+ self .check_git_repository ()
224+ if self .stack_id :
225+ self .update_to_portainer (rq = rq , base_url = base_url , jwt = jwt , prune = prune , repositoryUsername = repositoryUsername , repositoryPassword = repositoryPassword )
226+ else :
227+ self .create_to_portainer (rq = rq , base_url = base_url , jwt = jwt , repositoryUsername = repositoryUsername , repositoryPassword = repositoryPassword )
228+ log .info ("update_or_create query ok" )
229+
230+
98231class CreateOrUpdateStack (EntryPoint ):
99232 """扫描指定目录下的compose文件,在指定的端点中如果已经部署则更新stack,否则创建stack."""
100233 default_config_file_paths = [
@@ -153,7 +286,10 @@ def do_main(self) -> None:
153286 repository_username = self .config .get ("repository_username" )
154287 repository_password = self .config .get ("repository_password" )
155288 retry_max_times = self .config .get ("retry_max_times" )
156- retry_interval = self .config .get ("retry_interval" )
289+ retry_interval_backoff_factor = self .config .get ("retry_interval_backoff_factor" )
290+ rq = requests .Session ()
291+ if retry_max_times and int (retry_max_times ) > 0 :
292+ rq .mount ('https://' , HTTPAdapter (max_retries = Retry (total = int (retry_max_times ), backoff_factor = retry_interval_backoff_factor , method_whitelist = frozenset (['GET' , 'POST' , 'PUT' ]))))
157293 # 初始化log
158294 log .initialize_for_app (app_name = "UpdateStack" , log_level = log_level )
159295 log .info ("get config" , config = self .config )
@@ -165,16 +301,16 @@ def do_main(self) -> None:
165301 endpoints_stacks = self .handdler_excepts (excepts )
166302 log .debug ("deal with excepts ok" , endpoints_stacks = endpoints_stacks )
167303 # 获取jwt
168- jwt = get_jwt (base_url = base_url , username = username , password = password )
304+ jwt = get_jwt (rq , base_url = base_url , username = username , password = password )
169305 log .debug ("deal with jwt ok" , jwt = jwt )
170306 # 获取已经存在的stack信息
171- endpoint_stack_info = get_all_stacks_from_portainer (base_url = base_url , jwt = jwt )
307+ endpoint_stack_info = get_all_stacks_from_portainer (rq , base_url = base_url , jwt = jwt )
172308 log .debug ("deal with endpoint_stack_info ok" , endpoint_stack_info = endpoint_stack_info )
173309 # 获取endpoint信息
174310 for endpoint in endpoints :
175311 swarmID : Optional [str ] = None
176312 try :
177- swarmID = get_swarm_id (base_url = base_url , jwt = jwt , endpoint = endpoint )
313+ swarmID = get_swarm_id (rq , base_url = base_url , jwt = jwt , endpoint = endpoint )
178314 except NotSwarmEndpointError :
179315 log .info ("Endpoint not swarm" , endpoint = endpoint )
180316 except Exception as e :
@@ -188,9 +324,8 @@ def do_main(self) -> None:
188324 stack = exist_stacks .get (_stack .stack_name )
189325 if stack :
190326 if stack .repositoryURL == repository_url and stack .repositoryReferenceName == repository_reference_name and stack .composeFilePathInRepository == _stack .path and stack .swarm_id == swarmID :
191- stack .update_or_create (base_url = base_url , jwt = jwt , prune = prune ,
192- repositoryUsername = repository_username , repositoryPassword = repository_password ,
193- retry_max_times = retry_max_times , retry_interval = retry_interval )
327+ stack .update_or_create (rq , base_url = base_url , jwt = jwt , prune = prune ,
328+ repositoryUsername = repository_username , repositoryPassword = repository_password )
194329 else :
195330 stack = GitStackInfo (
196331 endpoint_id = endpoint ,
@@ -199,7 +334,107 @@ def do_main(self) -> None:
199334 composeFilePathInRepository = _stack .path ,
200335 repositoryReferenceName = repository_reference_name ,
201336 repositoryURL = repository_url , env = [])
202- stack .update_or_create (
203- base_url = base_url , jwt = jwt , prune = prune ,
204- repositoryUsername = repository_username , repositoryPassword = repository_password ,
205- retry_max_times = retry_max_times , retry_interval = retry_interval )
337+ stack .update_or_create (rq , base_url = base_url , jwt = jwt , prune = prune ,
338+ repositoryUsername = repository_username , repositoryPassword = repository_password )
339+
340+
341+ def get_swarm_id (rq : requests .Session , base_url : str , jwt : str , endpoint : int ) -> str :
342+ """获取端点的SwarmID.
343+
344+ Args:
345+ rq (requests.Session): 请求会话
346+ base_url (str): portainer的根地址
347+ jwt (str): 访问jwt
348+ endpoint (int): 端点ID
349+
350+ Raises:
351+ HttpCodeError: get swarm id query get error
352+ e: get swarm id query get json result error
353+ AssertionError: endpint not swarm
354+
355+ Returns:
356+ str: Swarm ID
357+ """
358+ res = rq .get (f"{ base_url } /api/endpoints/{ endpoint } /docker/swarm" ,
359+ headers = requests .structures .CaseInsensitiveDict ({"Authorization" : "Bearer " + jwt }))
360+ if res .status_code != 200 :
361+ log .error ("get swarm id query get error" ,
362+ base_url = base_url ,
363+ endpoint = endpoint ,
364+ status_code = res .status_code )
365+ raise HttpCodeError ("get swarm id query get error" )
366+ try :
367+ res_json = res .json ()
368+ except Exception as e :
369+ log .error ("get swarm id query get json result error" , endpoint = endpoint , err = type (e ), err_msg = str (e ), exc_info = True , stack_info = True )
370+ raise e
371+ else :
372+ swarm_id = res_json .get ("ID" )
373+ if swarm_id :
374+ return swarm_id
375+ else :
376+ raise NotSwarmEndpointError (f"endpint { endpoint } not swarm" )
377+
378+
379+ def get_all_stacks_from_portainer (rq : requests .Session , base_url : str , jwt : str ) -> Dict [int , Dict [str , GitStackInfo ]]:
380+ """
381+
382+ Args:
383+ rq (requests.Session): 请求会话
384+ base_url (str): portainer的根地址
385+ jwt (str): 访问jwt
386+
387+ Raises:
388+ HttpCodeError: get stack query get error
389+ e: get stack query get json result error
390+
391+ Returns:
392+ Dict[int, Dict[str, GitStackInfo]]: dict[endpointid,dict[stack_name,stackinfo]]
393+ """
394+ res = rq .get (f"{ base_url } /api/stacks" ,
395+ headers = requests .structures .CaseInsensitiveDict ({"Authorization" : "Bearer " + jwt }))
396+ if res .status_code != 200 :
397+ log .error ("get swarm id query get error" ,
398+ base_url = base_url ,
399+ status_code = res .status_code )
400+ raise HttpCodeError ("get stack query get error" )
401+ try :
402+ res_jsons = res .json ()
403+ except Exception as e :
404+ log .error ("get stack query get json result error" , err = type (e ), err_msg = str (e ), exc_info = True , stack_info = True )
405+ raise e
406+ else :
407+ result : Dict [int , Dict [str , GitStackInfo ]] = {}
408+ for res_json in res_jsons :
409+ gcf = res_json .get ('GitConfig' )
410+ endpoint_id = res_json ['EndpointId' ]
411+ stack_id = res_json ['Id' ]
412+ stack_name = res_json ["Name" ]
413+ if gcf :
414+ gsi = GitStackInfo (
415+ endpoint_id = endpoint_id ,
416+ env = res_json ["Env" ],
417+ stack_id = stack_id ,
418+ stack_name = stack_name ,
419+ swarm_id = res_json .get ('SwarmId' ),
420+ composeFilePathInRepository = gcf ["ConfigFilePath" ],
421+ repositoryReferenceName = gcf ["ReferenceName" ],
422+ repositoryURL = gcf ["URL" ]
423+ )
424+ else :
425+ gsi = GitStackInfo (
426+ endpoint_id = endpoint_id ,
427+ env = res_json ["Env" ],
428+ stack_id = stack_id ,
429+ stack_name = res_json ["Name" ],
430+ swarm_id = res_json .get ('SwarmId' ),
431+ composeFilePathInRepository = None ,
432+ repositoryReferenceName = None ,
433+ repositoryURL = None
434+ )
435+ if not result .get (endpoint_id ):
436+ result [endpoint_id ] = {stack_name : gsi }
437+ else :
438+ result [endpoint_id ][stack_name ] = gsi
439+
440+ return result
0 commit comments