Skip to content

Commit fa2e549

Browse files
committed
update
1 parent c080baa commit fa2e549

File tree

7 files changed

+325
-346
lines changed

7 files changed

+325
-346
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# 0.0.2
2+
3+
## 接口修改
4+
5+
+ `updateservicebywebhook`现在可以设置多个token
6+
+ `artifact_version``tag_prefix`参数被取消
7+
+ 现在`retry_max_times``retry_interval_backoff_factor`作为哦三个子命令的共有参数,`retry_interval`被移除,现在重试的间隔时间将根据公式`{backoff factor} * (2 ** ({number of total retries} - 1))`获得
8+
19
# 0.0.1
210

311
项目创建

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ docker镜像中已经申明了`ENTRYPOINT [ "python","-m", "portainer_deploy_too
1212

1313
+ `updateserviceinstack`用于更新某个服务对应的镜像后根据指定的路径更新stack
1414

15-
+ `updateservicebywebhooks`用于在portainer上激活webhook后通过调用webhook更新服务
15+
+ `updateservicebywebhooks`用于在portainer上激活webhook后通过调用webhook更新服务(不建议指定tag,这会让stack和实际执行不一致)
1616

1717
+ `createorupdatestack`用于根据目录下的指定后缀的文件来创建或者更新stack配置.
1818

portainer_deploy_tool/create_or_update_stack.py

Lines changed: 261 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
from pathlib import Path
2+
from dataclasses import dataclass
23
from copy import deepcopy
34
from collections import defaultdict
45
from 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
69
from schema_entry import EntryPoint
710
from pyloggerhelper import log
811
from .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

@@ -75,26 +75,159 @@
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

9081
schema_properties.update(**base_schema_properties)
9182

9283

84+
class NotSwarmEndpointError(Exception):
85+
"""节点不是swarm节点"""
86+
pass
87+
88+
9389
class 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+
98231
class 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

Comments
 (0)