diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b2f194..bad3551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 0.0.2 + +## 接口修改 + ++ `updateservicebywebhook`现在可以设置多个token ++ `artifact_version`和`tag_prefix`参数被取消 ++ 现在`retry_max_times`和`retry_interval_backoff_factor`作为哦三个子命令的共有参数,`retry_interval`被移除,现在重试的间隔时间将根据公式`{backoff factor} * (2 ** ({number of total retries} - 1))`获得 + # 0.0.1 项目创建 \ No newline at end of file diff --git a/README.md b/README.md index 876ec8e..dd2179e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ docker镜像中已经申明了`ENTRYPOINT [ "python","-m", "portainer_deploy_too + `updateserviceinstack`用于更新某个服务对应的镜像后根据指定的路径更新stack -+ `updateservicebywebhooks`用于在portainer上激活webhook后通过调用webhook更新服务 ++ `updateservicebywebhooks`用于在portainer上激活webhook后通过调用webhook更新服务(不建议指定tag,这会让stack和实际执行不一致) + `createorupdatestack`用于根据目录下的指定后缀的文件来创建或者更新stack配置. diff --git a/portainer_deploy_tool/create_or_update_stack.py b/portainer_deploy_tool/create_or_update_stack.py index 9fb9ef8..5a883bd 100644 --- a/portainer_deploy_tool/create_or_update_stack.py +++ b/portainer_deploy_tool/create_or_update_stack.py @@ -1,17 +1,17 @@ from pathlib import Path +from dataclasses import dataclass from copy import deepcopy from collections import defaultdict from typing import Dict, Any, List, NamedTuple, DefaultDict, Optional -import requests as rq +import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util import Retry from schema_entry import EntryPoint from pyloggerhelper import log from .utils import ( - GitStackInfo, + HttpCodeError, base_schema_properties, - get_jwt, - get_swarm_id, - get_all_stacks_from_portainer, - NotSwarmEndpointError + get_jwt ) @@ -75,26 +75,159 @@ "type": "string", "description": "执行位置", "default": "." - }, - "retry_max_times": { - "type": "integer", - "description": "重试次数", - }, - "retry_interval": { - "type": "number", - "description": "重试间隔时间", - "default": 3 } } schema_properties.update(**base_schema_properties) +class NotSwarmEndpointError(Exception): + """节点不是swarm节点""" + pass + + class ComposeInfo(NamedTuple): stack_name: str path: str +@dataclass +class GitStackInfo: + endpoint_id: int + stack_name: str + stack_id: Optional[int] = None + swarm_id: Optional[str] = None + composeFilePathInRepository: Optional[str] = None + repositoryReferenceName: Optional[str] = None + repositoryURL: Optional[str] = None + env: Optional[List[Dict[str, str]]] = None + + def check_git_repository(self) -> None: + if not (self.composeFilePathInRepository and self.repositoryReferenceName and self.repositoryURL): + raise AttributeError("need git repository info") + + def update_to_portainer(self, rq: requests.Session, base_url: str, jwt: str, prune: bool = False, + repositoryUsername: Optional[str] = None, repositoryPassword: Optional[str] = None) -> None: + """更新portainer中的stack + + Args: + base_url (str): portainer的根地址 + jwt (str): 访问jwt + prune (bool, optional): 更新是否删除不再使用的资源. Defaults to False. + repositoryUsername (Optional[str], optional): git仓库账户. Defaults to None. + repositoryPassword (Optional[str], optional): git仓库密码. Defaults to None. + + Raises: + HttpCodeError: update stack query get error + e: update stack query get json result error + """ + body: Dict[str, Any] = { + "env": self.env, + "prune": prune, + "repositoryReferenceName": self.repositoryReferenceName, + } + if repositoryUsername and repositoryPassword: + repositoryAuthentication = True + body.update({ + "repositoryAuthentication": repositoryAuthentication, + "repositoryPassword": repositoryPassword, + "repositoryUsername": repositoryUsername + }) + else: + repositoryAuthentication = False + body.update({ + "repositoryAuthentication": repositoryAuthentication + }) + res = rq.put( + f"{base_url}/api/stacks/{self.stack_id}/git", + headers=requests.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt}), + params={ + "endpointId": self.endpoint_id + }, + json=body) + if res.status_code != 200: + log.error("update stack query get error", + base_url=base_url, + status_code=res.status_code, + stack=self) + raise HttpCodeError("update stack query get error") + try: + res_json = res.json() + except Exception as e: + log.error("update stack query get json result error", stack=self, err=type(e), err_msg=str(e), exc_info=True, stack_info=True) + raise e + else: + log.info("update stack ok", stack=self, result=res_json) + + def create_to_portainer(self, rq: requests.Session, base_url: str, jwt: str, + repositoryUsername: Optional[str] = None, repositoryPassword: Optional[str] = None) -> None: + """使用对象的信息创建stack. + + Args: + base_url (str): portainer的根地址 + jwt (str): 访问jwt + repositoryUsername (Optional[str], optional): git仓库账户. Defaults to None. + repositoryPassword (Optional[str], optional): git仓库密码. Defaults to None. + + Raises: + AttributeError: only create git repository method + HttpCodeError: create stack query get error + e: create stack query get json result error + """ + body: Dict[str, Any] = { + "env": self.env, + "composeFilePathInRepository": self.composeFilePathInRepository, + "name": self.stack_name, + "repositoryReferenceName": self.repositoryReferenceName, + "repositoryURL": self.repositoryURL, + } + if repositoryUsername and repositoryPassword: + repositoryAuthentication = True + body.update({ + "repositoryAuthentication": repositoryAuthentication, + "repositoryPassword": repositoryPassword, + "repositoryUsername": repositoryUsername + }) + else: + repositoryAuthentication = False + body.update({ + "repositoryAuthentication": repositoryAuthentication + }) + if self.swarm_id: + stack_type = 1 + body.update({"swarmID": self.swarm_id}) + else: + stack_type = 2 + params = (("method", "repository"), ("type", stack_type), ("endpointId", self.endpoint_id)) + res = rq.post( + f"{base_url}/api/stacks", + headers=requests.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt}), + params=params, + json=body) + if res.status_code != 200: + log.error("create stack query get error", + base_url=base_url, + status_code=res.status_code, + stack=self) + raise HttpCodeError("create stack query get error") + try: + res_json = res.json() + except Exception as e: + log.error("create stack query get json result error", stack=self, err=type(e), err_msg=str(e), exc_info=True, stack_info=True) + raise e + else: + log.info("create stack ok", stack=self, result=res_json) + + def update_or_create(self, rq: requests.Session, base_url: str, jwt: str, prune: bool = False, + repositoryUsername: Optional[str] = None, repositoryPassword: Optional[str] = None) -> None: + self.check_git_repository() + if self.stack_id: + self.update_to_portainer(rq=rq, base_url=base_url, jwt=jwt, prune=prune, repositoryUsername=repositoryUsername, repositoryPassword=repositoryPassword) + else: + self.create_to_portainer(rq=rq, base_url=base_url, jwt=jwt, repositoryUsername=repositoryUsername, repositoryPassword=repositoryPassword) + log.info("update_or_create query ok") + + class CreateOrUpdateStack(EntryPoint): """扫描指定目录下的compose文件,在指定的端点中如果已经部署则更新stack,否则创建stack.""" default_config_file_paths = [ @@ -153,7 +286,10 @@ def do_main(self) -> None: repository_username = self.config.get("repository_username") repository_password = self.config.get("repository_password") retry_max_times = self.config.get("retry_max_times") - retry_interval = self.config.get("retry_interval") + retry_interval_backoff_factor = self.config.get("retry_interval_backoff_factor") + rq = requests.Session() + if retry_max_times and int(retry_max_times) > 0: + rq.mount('https://', HTTPAdapter(max_retries=Retry(total=int(retry_max_times), backoff_factor=retry_interval_backoff_factor, method_whitelist=frozenset(['GET', 'POST', 'PUT'])))) # 初始化log log.initialize_for_app(app_name="UpdateStack", log_level=log_level) log.info("get config", config=self.config) @@ -165,16 +301,16 @@ def do_main(self) -> None: endpoints_stacks = self.handdler_excepts(excepts) log.debug("deal with excepts ok", endpoints_stacks=endpoints_stacks) # 获取jwt - jwt = get_jwt(base_url=base_url, username=username, password=password) + jwt = get_jwt(rq, base_url=base_url, username=username, password=password) log.debug("deal with jwt ok", jwt=jwt) # 获取已经存在的stack信息 - endpoint_stack_info = get_all_stacks_from_portainer(base_url=base_url, jwt=jwt) + endpoint_stack_info = get_all_stacks_from_portainer(rq, base_url=base_url, jwt=jwt) log.debug("deal with endpoint_stack_info ok", endpoint_stack_info=endpoint_stack_info) # 获取endpoint信息 for endpoint in endpoints: swarmID: Optional[str] = None try: - swarmID = get_swarm_id(base_url=base_url, jwt=jwt, endpoint=endpoint) + swarmID = get_swarm_id(rq, base_url=base_url, jwt=jwt, endpoint=endpoint) except NotSwarmEndpointError: log.info("Endpoint not swarm", endpoint=endpoint) except Exception as e: @@ -188,9 +324,8 @@ def do_main(self) -> None: stack = exist_stacks.get(_stack.stack_name) if stack: if stack.repositoryURL == repository_url and stack.repositoryReferenceName == repository_reference_name and stack.composeFilePathInRepository == _stack.path and stack.swarm_id == swarmID: - stack.update_or_create(base_url=base_url, jwt=jwt, prune=prune, - repositoryUsername=repository_username, repositoryPassword=repository_password, - retry_max_times=retry_max_times, retry_interval=retry_interval) + stack.update_or_create(rq, base_url=base_url, jwt=jwt, prune=prune, + repositoryUsername=repository_username, repositoryPassword=repository_password) else: stack = GitStackInfo( endpoint_id=endpoint, @@ -199,7 +334,107 @@ def do_main(self) -> None: composeFilePathInRepository=_stack.path, repositoryReferenceName=repository_reference_name, repositoryURL=repository_url, env=[]) - stack.update_or_create( - base_url=base_url, jwt=jwt, prune=prune, - repositoryUsername=repository_username, repositoryPassword=repository_password, - retry_max_times=retry_max_times, retry_interval=retry_interval) + stack.update_or_create(rq, base_url=base_url, jwt=jwt, prune=prune, + repositoryUsername=repository_username, repositoryPassword=repository_password) + + +def get_swarm_id(rq: requests.Session, base_url: str, jwt: str, endpoint: int) -> str: + """获取端点的SwarmID. + + Args: + rq (requests.Session): 请求会话 + base_url (str): portainer的根地址 + jwt (str): 访问jwt + endpoint (int): 端点ID + + Raises: + HttpCodeError: get swarm id query get error + e: get swarm id query get json result error + AssertionError: endpint not swarm + + Returns: + str: Swarm ID + """ + res = rq.get(f"{base_url}/api/endpoints/{endpoint}/docker/swarm", + headers=requests.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt})) + if res.status_code != 200: + log.error("get swarm id query get error", + base_url=base_url, + endpoint=endpoint, + status_code=res.status_code) + raise HttpCodeError("get swarm id query get error") + try: + res_json = res.json() + except Exception as e: + 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) + raise e + else: + swarm_id = res_json.get("ID") + if swarm_id: + return swarm_id + else: + raise NotSwarmEndpointError(f"endpint {endpoint} not swarm") + + +def get_all_stacks_from_portainer(rq: requests.Session, base_url: str, jwt: str) -> Dict[int, Dict[str, GitStackInfo]]: + """ + + Args: + rq (requests.Session): 请求会话 + base_url (str): portainer的根地址 + jwt (str): 访问jwt + + Raises: + HttpCodeError: get stack query get error + e: get stack query get json result error + + Returns: + Dict[int, Dict[str, GitStackInfo]]: dict[endpointid,dict[stack_name,stackinfo]] + """ + res = rq.get(f"{base_url}/api/stacks", + headers=requests.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt})) + if res.status_code != 200: + log.error("get swarm id query get error", + base_url=base_url, + status_code=res.status_code) + raise HttpCodeError("get stack query get error") + try: + res_jsons = res.json() + except Exception as e: + log.error("get stack query get json result error", err=type(e), err_msg=str(e), exc_info=True, stack_info=True) + raise e + else: + result: Dict[int, Dict[str, GitStackInfo]] = {} + for res_json in res_jsons: + gcf = res_json.get('GitConfig') + endpoint_id = res_json['EndpointId'] + stack_id = res_json['Id'] + stack_name = res_json["Name"] + if gcf: + gsi = GitStackInfo( + endpoint_id=endpoint_id, + env=res_json["Env"], + stack_id=stack_id, + stack_name=stack_name, + swarm_id=res_json.get('SwarmId'), + composeFilePathInRepository=gcf["ConfigFilePath"], + repositoryReferenceName=gcf["ReferenceName"], + repositoryURL=gcf["URL"] + ) + else: + gsi = GitStackInfo( + endpoint_id=endpoint_id, + env=res_json["Env"], + stack_id=stack_id, + stack_name=res_json["Name"], + swarm_id=res_json.get('SwarmId'), + composeFilePathInRepository=None, + repositoryReferenceName=None, + repositoryURL=None + ) + if not result.get(endpoint_id): + result[endpoint_id] = {stack_name: gsi} + else: + result[endpoint_id][stack_name] = gsi + + return result diff --git a/portainer_deploy_tool/update_service_by_webhook.py b/portainer_deploy_tool/update_service_by_webhook.py index 17e70d5..91b636a 100644 --- a/portainer_deploy_tool/update_service_by_webhook.py +++ b/portainer_deploy_tool/update_service_by_webhook.py @@ -1,27 +1,21 @@ -from typing import Dict, Any, Optional -import requests as rq +from typing import Dict, Any +import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util import Retry from schema_entry import EntryPoint from pyloggerhelper import log -from .utils import base_schema_properties +from .utils import base_schema_properties, HttpCodeError schema_properties: Dict[str, Any] = { - "token": { - "type": "string", + "tokens": { + "type": "array", + "items": { + "type": "string" + }, "title": "o", "description": "webhook的token" }, - "artifact_version": { - "type": "string", - "title": "v", - "description": "要更新的镜像版本", - "default": "latest" - }, - "tag_prefix": { - "type": "string", - "title": "t", - "description": "待更新路径" - } } schema_properties.update(**base_schema_properties) @@ -32,39 +26,24 @@ class UpdateServiceByWebhooks(EntryPoint): schema = { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", - "required": ["base_url", "token"], + "required": ["base_url", "tokens"], "properties": schema_properties } - def make_tag_name(self, artifact_version: str, *, tag_prefix: Optional[str] = None) -> str: - """构造service要更新的tag. - - Args: - artifact_version (str): 制品版本 - tag_prefix (Optional[str], optional): 制品版本标签前缀. Defaults to None. - - Returns: - str: 完整tag名 - """ - if tag_prefix: - tag_name = f"{tag_prefix}-{artifact_version}" - else: - tag_name = f"{artifact_version}" - - return tag_name - def do_main(self) -> None: """入口程序.""" base_url = self.config["base_url"] log_level = self.config["log_level"] - token = self.config["token"] + tokens = self.config["tokens"] + retry_max_times = self.config.get("retry_max_times") + retry_interval_backoff_factor = self.config.get("retry_interval_backoff_factor", 0) + rq = requests.Session() + if retry_max_times and int(retry_max_times) > 0: + rq.mount('https://', HTTPAdapter(max_retries=Retry(total=int(retry_max_times), backoff_factor=retry_interval_backoff_factor, method_whitelist=frozenset(['GET', 'POST', 'PUT'])))) log.initialize_for_app(app_name="UpdateStack", log_level=log_level) - if self.config.get("artifact_version"): - tag = self.make_tag_name(artifact_version=self.config.get("artifact_version"), tag_prefix=self.config.get("tag_prefix")) - res = rq.post(f"{base_url}/webhooks/{token}?tag={tag}") - else: + for token in tokens: res = rq.post(f"{base_url}/webhooks/{token}") - if res.status_code >= 300 or res.status_code <= 199: - log.error("update service query get error", status_code=res.status_code) - else: - log.info("update service query ok", status_code=res.status_code) + if res.status_code >= 300 or res.status_code <= 199: + log.error("update service query get error", status_code=res.status_code) + else: + log.info("update service query ok", status_code=res.status_code) diff --git a/portainer_deploy_tool/update_service_in_stack.py b/portainer_deploy_tool/update_service_in_stack.py index 943e2be..6239a1d 100644 --- a/portainer_deploy_tool/update_service_in_stack.py +++ b/portainer_deploy_tool/update_service_in_stack.py @@ -1,6 +1,8 @@ from typing import Dict, Any, List, Optional import yaml -import requests as rq +import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util import Retry from schema_entry import EntryPoint from pyloggerhelper import log from .utils import base_schema_properties, get_jwt, HttpCodeError @@ -115,10 +117,11 @@ def make_image_name(self, artifact_name: str, artifact_version: str, *, image_name = f"{artifact_name}:{artifact_version}" return image_name - def get_stack_file_content(self, base_url: str, jwt: str, stack_id: str) -> str: + def get_stack_file_content(self, rq: requests.Session, base_url: str, jwt: str, stack_id: str) -> str: """获取更新前stack的compose文本. Args: + rq (requests.Session): 请求会话 base_url (str): portainer基础路径 jwt (str): 访问jwt stack_id (str): stack信息的id @@ -133,7 +136,7 @@ def get_stack_file_content(self, base_url: str, jwt: str, stack_id: str) -> str: """ res = rq.get( f"{base_url}/stacks/{stack_id}/file", - headers=rq.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt}) + headers=requests.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt}) ) if res.status_code != 200: log.error("get stack file content query get error", stack_id=stack_id, status_code=res.status_code) @@ -151,10 +154,11 @@ def get_stack_file_content(self, base_url: str, jwt: str, stack_id: str) -> str: log.error("get stack file content query has no field StackFileContent", stack_id=stack_id) raise AttributeError("get stack file content query has no field StackFileContent") - def deploy(self, base_url: str, jwt: str, image_name: str, stack_key: str, services: List[str]) -> None: + def deploy(self, rq: requests.Session, base_url: str, jwt: str, image_name: str, stack_key: str, services: List[str]) -> None: """更新部署单个stack中的服务. Args: + rq (requests.Session): 请求会话 base_url (str): portainer基础路径 jwt (str): 访问jwt image_name (str): 镜像完整名 @@ -168,7 +172,7 @@ def deploy(self, base_url: str, jwt: str, image_name: str, stack_key: str, servi """ endpoint_id_str, stack_id = stack_key.split("::") endpoint_id = int(endpoint_id_str) - StackFileContent = self.get_stack_file_content(base_url=base_url, jwt=jwt, stack_id=stack_id) + StackFileContent = self.get_stack_file_content(rq, base_url=base_url, jwt=jwt, stack_id=stack_id) s = yaml.load(StackFileContent) log.info("get old compose", content=s) for service_name in services: @@ -180,7 +184,7 @@ def deploy(self, base_url: str, jwt: str, image_name: str, stack_key: str, servi compose = yaml.dump(s, sort_keys=False) res = rq.put( f"{base_url}/stacks/{stack_id}", - headers=rq.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt}), + headers=requests.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt}), params={"endpointId": endpoint_id}, json={ "StackFileContent": compose, @@ -200,10 +204,11 @@ def deploy(self, base_url: str, jwt: str, image_name: str, stack_key: str, servi log.debug("deploy query get result", stack_key=stack_key, cotent=res_json) log.info("deploy ok", stack_key=stack_key) - def deploy_all(self, base_url: str, jwt: str, image_name: str, stack_services: Dict[str, List[str]]) -> None: + def deploy_all(self, rq: requests.Session, base_url: str, jwt: str, image_name: str, stack_services: Dict[str, List[str]]) -> None: """部署解析出来的所有stack. Args: + rq (requests.Session): 请求会话 base_url (str): portainer基础路径 jwt (str): 访问jwt image_name (str): 镜像完整名 @@ -211,6 +216,7 @@ def deploy_all(self, base_url: str, jwt: str, image_name: str, stack_services: D """ for stack_key, services in stack_services.items(): self.deploy( + rq, base_url=base_url, jwt=jwt, image_name=image_name, @@ -227,8 +233,13 @@ def do_main(self) -> None: artifact_name = self.config["artifact_name"] artifact_version = self.config["artifact_version"] deploy_path = self.config["deploy_path"] + retry_max_times = self.config.get("retry_max_times") + retry_interval_backoff_factor = self.config.get("retry_interval_backoff_factor") + rq = requests.Session() + if retry_max_times and int(retry_max_times) > 0: + rq.mount('https://', HTTPAdapter(max_retries=Retry(total=int(retry_max_times), backoff_factor=retry_interval_backoff_factor, method_whitelist=frozenset(['GET', 'POST', 'PUT'])))) log.initialize_for_app(app_name="UpdateStack", log_level=log_level) - jwt = get_jwt(base_url=base_url, username=username, password=password) + jwt = get_jwt(rq, base_url=base_url, username=username, password=password) image_name = self.make_image_name( artifact_name=artifact_name, artifact_version=artifact_version, @@ -237,6 +248,7 @@ def do_main(self) -> None: ) stack_services = self.parser_deploy_path(deploy_path) self.deploy_all( + rq, base_url=base_url, jwt=jwt, image_name=image_name, diff --git a/portainer_deploy_tool/utils.py b/portainer_deploy_tool/utils.py index dbc47bf..b162e39 100644 --- a/portainer_deploy_tool/utils.py +++ b/portainer_deploy_tool/utils.py @@ -1,8 +1,5 @@ -import time -from dataclasses import dataclass -import requests as rq +import requests from pyloggerhelper import log -from typing import Dict, Any, List, Union, Optional base_schema_properties = { "log_level": { @@ -16,6 +13,15 @@ "type": "string", "title": "b", "description": "portainer的根url" + }, + "retry_max_times": { + "type": "integer", + "description": "重试次数", + }, + "retry_interval_backoff_factor": { + "type": "number", + "description": "重试间隔时间,的参数,间隔时间位`{backoff factor} * (2 ** ({number of total retries} - 1))`", + "default": 0.1 } } @@ -25,172 +31,11 @@ class HttpCodeError(Exception): pass -class NotSwarmEndpointError(Exception): - """节点不是swarm节点""" - pass - - -@dataclass -class GitStackInfo: - endpoint_id: int - stack_name: str - stack_id: Optional[int] = None - swarm_id: Optional[str] = None - composeFilePathInRepository: Optional[str] = None - repositoryReferenceName: Optional[str] = None - repositoryURL: Optional[str] = None - env: Optional[List[Dict[str, str]]] = None - - def check_git_repository(self) -> None: - if not (self.composeFilePathInRepository and self.repositoryReferenceName and self.repositoryURL): - raise AttributeError("need git repository info") - - def update_to_portainer(self, base_url: str, jwt: str, prune: bool = False, - repositoryUsername: Optional[str] = None, repositoryPassword: Optional[str] = None) -> None: - """更新portainer中的stack - - Args: - base_url (str): portainer的根地址 - jwt (str): 访问jwt - prune (bool, optional): 更新是否删除不再使用的资源. Defaults to False. - repositoryUsername (Optional[str], optional): git仓库账户. Defaults to None. - repositoryPassword (Optional[str], optional): git仓库密码. Defaults to None. - - Raises: - HttpCodeError: update stack query get error - e: update stack query get json result error - """ - body: Dict[str, Any] = { - "env": self.env, - "prune": prune, - "repositoryReferenceName": self.repositoryReferenceName, - } - if repositoryUsername and repositoryPassword: - repositoryAuthentication = True - body.update({ - "repositoryAuthentication": repositoryAuthentication, - "repositoryPassword": repositoryPassword, - "repositoryUsername": repositoryUsername - }) - else: - repositoryAuthentication = False - body.update({ - "repositoryAuthentication": repositoryAuthentication - }) - res = rq.put( - f"{base_url}/api/stacks/{self.stack_id}/git", - headers=rq.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt}), - params={ - "endpointId": self.endpoint_id - }, - json=body) - if res.status_code != 200: - log.error("update stack query get error", - base_url=base_url, - status_code=res.status_code, - stack=self) - raise HttpCodeError("update stack query get error") - try: - res_json = res.json() - except Exception as e: - log.error("update stack query get json result error", stack=self, err=type(e), err_msg=str(e), exc_info=True, stack_info=True) - raise e - else: - log.info("update stack ok", stack=self, result=res_json) - - def create_to_portainer(self, base_url: str, jwt: str, - repositoryUsername: Optional[str] = None, repositoryPassword: Optional[str] = None) -> None: - """使用对象的信息创建stack. - - Args: - base_url (str): portainer的根地址 - jwt (str): 访问jwt - repositoryUsername (Optional[str], optional): git仓库账户. Defaults to None. - repositoryPassword (Optional[str], optional): git仓库密码. Defaults to None. - - Raises: - AttributeError: only create git repository method - HttpCodeError: create stack query get error - e: create stack query get json result error - """ - body: Dict[str, Any] = { - "env": self.env, - "composeFilePathInRepository": self.composeFilePathInRepository, - "name": self.stack_name, - "repositoryReferenceName": self.repositoryReferenceName, - "repositoryURL": self.repositoryURL, - } - if repositoryUsername and repositoryPassword: - repositoryAuthentication = True - body.update({ - "repositoryAuthentication": repositoryAuthentication, - "repositoryPassword": repositoryPassword, - "repositoryUsername": repositoryUsername - }) - else: - repositoryAuthentication = False - body.update({ - "repositoryAuthentication": repositoryAuthentication - }) - if self.swarm_id: - stack_type = 1 - body.update({"swarmID": self.swarm_id}) - else: - stack_type = 2 - params = (("method", "repository"), ("type", stack_type), ("endpointId", self.endpoint_id)) - res = rq.post( - f"{base_url}/api/stacks", - headers=rq.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt}), - params=params, - json=body) - if res.status_code != 200: - log.error("create stack query get error", - base_url=base_url, - status_code=res.status_code, - stack=self) - raise HttpCodeError("create stack query get error") - try: - res_json = res.json() - except Exception as e: - log.error("create stack query get json result error", stack=self, err=type(e), err_msg=str(e), exc_info=True, stack_info=True) - raise e - else: - log.info("create stack ok", stack=self, result=res_json) - - def update_or_create(self, base_url: str, jwt: str, prune: bool = False, - repositoryUsername: Optional[str] = None, repositoryPassword: Optional[str] = None, - retry_max_times: Optional[int] = None, retry_interval: Union[int, float] = 3) -> None: - self.check_git_repository() - if retry_max_times and int(retry_max_times) > 0: - for rt in range(int(retry_max_times)): - try: - if self.stack_id: - self.update_to_portainer(base_url=base_url, jwt=jwt, prune=prune, repositoryUsername=repositoryUsername, repositoryPassword=repositoryPassword) - else: - self.create_to_portainer(base_url=base_url, jwt=jwt, repositoryUsername=repositoryUsername, repositoryPassword=repositoryPassword) - except HttpCodeError as he: - raise he - except Exception as e: - log.warn("update_or_create query get error, retry", retry_times=rt + 1, retry_max_times=int(retry_max_times), - err=type(e), err_msg=str(e), exc_info=True, stack_info=True) - time.sleep(retry_interval) - continue - else: - break - log.info("update_or_create query ok") - - else: - if self.stack_id: - self.update_to_portainer(base_url=base_url, jwt=jwt, prune=prune, repositoryUsername=repositoryUsername, repositoryPassword=repositoryPassword) - else: - self.create_to_portainer(base_url=base_url, jwt=jwt, repositoryUsername=repositoryUsername, repositoryPassword=repositoryPassword) - log.info("update_or_create query ok") - - -def get_jwt(base_url: str, username: str, password: str) -> str: +def get_jwt(rq: requests.Session, base_url: str, username: str, password: str) -> str: """获取jwt. Args: + rq (requests.Session): 请求会话 base_url (str): portainer的根地址 username (str): portainer用户名 password (str): 用户的密码 @@ -232,103 +77,3 @@ def get_jwt(base_url: str, username: str, password: str) -> str: username=username, res_json=res_json) raise AttributeError("get jwt query has no field jwt") - - -def get_swarm_id(base_url: str, jwt: str, endpoint: int) -> str: - """获取端点的SwarmID. - - Args: - base_url (str): portainer的根地址 - jwt (str): 访问jwt - endpoint (int): 端点ID - - Raises: - HttpCodeError: get swarm id query get error - e: get swarm id query get json result error - AssertionError: endpint not swarm - - Returns: - str: Swarm ID - """ - res = rq.get(f"{base_url}/api/endpoints/{endpoint}/docker/swarm", - headers=rq.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt})) - if res.status_code != 200: - log.error("get swarm id query get error", - base_url=base_url, - endpoint=endpoint, - status_code=res.status_code) - raise HttpCodeError("get swarm id query get error") - try: - res_json = res.json() - except Exception as e: - 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) - raise e - else: - swarm_id = res_json.get("ID") - if swarm_id: - return swarm_id - else: - raise NotSwarmEndpointError(f"endpint {endpoint} not swarm") - - -def get_all_stacks_from_portainer(base_url: str, jwt: str) -> Dict[int, Dict[str, GitStackInfo]]: - """ - - Args: - base_url (str): portainer的根地址 - jwt (str): 访问jwt - - Raises: - HttpCodeError: get stack query get error - e: get stack query get json result error - - Returns: - Dict[int, Dict[str, GitStackInfo]]: dict[endpointid,dict[stack_name,stackinfo]] - """ - res = rq.get(f"{base_url}/api/stacks", - headers=rq.structures.CaseInsensitiveDict({"Authorization": "Bearer " + jwt})) - if res.status_code != 200: - log.error("get swarm id query get error", - base_url=base_url, - status_code=res.status_code) - raise HttpCodeError("get stack query get error") - try: - res_jsons = res.json() - except Exception as e: - log.error("get stack query get json result error", err=type(e), err_msg=str(e), exc_info=True, stack_info=True) - raise e - else: - result: Dict[int, Dict[str, GitStackInfo]] = {} - for res_json in res_jsons: - gcf = res_json.get('GitConfig') - endpoint_id = res_json['EndpointId'] - stack_id = res_json['Id'] - stack_name = res_json["Name"] - if gcf: - gsi = GitStackInfo( - endpoint_id=endpoint_id, - env=res_json["Env"], - stack_id=stack_id, - stack_name=stack_name, - swarm_id=res_json.get('SwarmId'), - composeFilePathInRepository=gcf["ConfigFilePath"], - repositoryReferenceName=gcf["ReferenceName"], - repositoryURL=gcf["URL"] - ) - else: - gsi = GitStackInfo( - endpoint_id=endpoint_id, - env=res_json["Env"], - stack_id=stack_id, - stack_name=res_json["Name"], - swarm_id=res_json.get('SwarmId'), - composeFilePathInRepository=None, - repositoryReferenceName=None, - repositoryURL=None - ) - if not result.get(endpoint_id): - result[endpoint_id] = {stack_name: gsi} - else: - result[endpoint_id][stack_name] = gsi - - return result diff --git a/setup.cfg b/setup.cfg index ddb3f29..e564451 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = portainer_deploy_tool -version = 0.0.1 +version = 0.0.2 author = hsz12 author_email = description =