app.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. from flask import Blueprint, request, g
  2. from apps.deploy.models import App, AppHostRel, DeployMenu
  3. from apps.configuration.models import ConfigKey, AppConfigRel, Environment
  4. from apps.assets.models import Host
  5. from libs.tools import json_response, JsonParser, Argument, AttrDict
  6. from libs.utils import Container
  7. from docker.errors import DockerException
  8. from datetime import datetime
  9. from public import db
  10. from libs.decorators import require_permission
  11. from apps.deploy.utils import get_built_in_menus
  12. blueprint = Blueprint(__name__, __name__)
  13. args = AttrDict(
  14. group=Argument('group', help='请选择分组名称!'),
  15. name=Argument('name', help='请输入应用名称!'),
  16. desc=Argument('desc', help='请输入应用描述!'),
  17. identify=Argument('identify', help='请输入应用标识!'),
  18. image_id=Argument('image_id', type=int, help='请选择应用使用的Docker镜像!')
  19. )
  20. @blueprint.route('/', methods=['GET'])
  21. @require_permission('publish_app_view | config_app_view | publish_field_rel_view')
  22. def get():
  23. group = request.args.get('group')
  24. query = App.query
  25. if group:
  26. query = query.filter_by(group=group)
  27. if g.user.is_supper:
  28. apps = query.all()
  29. else:
  30. app_ids = g.user.role.app_ids
  31. if app_ids:
  32. apps = query.filter(App.id.in_(app_ids.split(','))).all()
  33. else:
  34. apps = []
  35. return json_response(apps)
  36. @blueprint.route('/', methods=['POST'])
  37. @require_permission('publish_app_add')
  38. def post():
  39. form, error = JsonParser(*args.values()).parse()
  40. if error is None:
  41. if App.query.filter_by(identify=form.identify).first():
  42. return json_response(message='应用标识不能重复!')
  43. app = App(**form)
  44. app.create_date = datetime.now()
  45. app.edit_date = datetime.now()
  46. app.save()
  47. if not g.user.is_supper:
  48. g.user.role.update(app_ids=g.user.role.app_ids + ',%d' % app.id)
  49. return json_response(app)
  50. return json_response(message=error)
  51. @blueprint.route('/<int:app_id>', methods=['DELETE'])
  52. @require_permission('publish_app_del')
  53. def delete(app_id):
  54. app = App.query.get_or_404(app_id)
  55. if AppHostRel.query.filter_by(app_id=app_id).first():
  56. return json_response(message='请先取消与已发布主机的关联后再尝试删除应用!')
  57. rel = AppConfigRel.query.filter_by(d_id=app_id, d_type='app').first()
  58. if rel:
  59. rel_app = App.query.get_or_404(rel.s_id)
  60. return json_response(message='应用 <%s> 引用了该应用,请解除关联后再尝试删除该应用!' % rel_app.name)
  61. app_keys = ConfigKey.query.filter_by(owner_id=app_id, owner_type='app')
  62. if [x for x in app_keys.all() if x.type != 'system']:
  63. return json_response(message='为了安全,请删除该应用下的所有配置后再尝试删除该应用!')
  64. app_keys.delete()
  65. app.delete()
  66. return json_response()
  67. @blueprint.route('/<int:app_id>', methods=['PUT'])
  68. @require_permission('publish_app_edit')
  69. def put(app_id):
  70. form, error = JsonParser(*args.values()).parse()
  71. if error is None:
  72. exists_record = App.query.filter_by(identify=form.identify).first()
  73. if exists_record and exists_record.id != app_id:
  74. return json_response(message='应用标识不能重复!')
  75. app = App.query.get_or_404(app_id)
  76. app.update(**form)
  77. app.save()
  78. return json_response(app)
  79. return json_response(message=error)
  80. @blueprint.route('/<int:app_id>/bind/hosts', methods=['POST'])
  81. @require_permission('publish_app_publish_view')
  82. def bind_hosts(app_id):
  83. form, error = JsonParser('ids', 'env_id').parse()
  84. if error is None:
  85. pro = App.query.get_or_404(app_id)
  86. env = Environment.query.get_or_404(form.env_id)
  87. old_relationships = AppHostRel.query.filter_by(env_id=form.env_id, app_id=app_id).all()[:]
  88. for host_id in form.ids:
  89. rel = AppHostRel(env_id=form.env_id, app_id=app_id, host_id=host_id)
  90. if rel in old_relationships:
  91. old_relationships.remove(rel)
  92. else:
  93. rel.add()
  94. for old_rel in old_relationships:
  95. host = Host.query.get_or_404(old_rel.host_id)
  96. try:
  97. app_process = Container(host.docker_uri, '%s.%s' % (pro.identify, env.identify)).info
  98. if app_process:
  99. return json_response(message='在主机 <%s> 上已经部署了该应用,请删除已部署的容器后再尝试解除关联!' % host.name)
  100. except DockerException:
  101. pass
  102. old_rel.delete(commit=False)
  103. db.session.commit()
  104. return json_response()
  105. return json_response(message=error)
  106. @blueprint.route('/groups/', methods=['GET'])
  107. @require_permission('publish_app_view')
  108. def fetch_groups():
  109. apps = db.session.query(App.group.distinct().label('group')).all()
  110. return json_response([x.group for x in apps])
  111. # 用户在发布详情页获取自定义的展示字段,需具有应用发布权限
  112. @blueprint.route('/<int:app_id>/fields', methods=['GET'])
  113. @require_permission('publish_app_publish_view')
  114. def fetch_fields(app_id):
  115. pro = App.query.get_or_404(app_id)
  116. return json_response(pro.fields)
  117. # 用于在发布详情页面获取启用的自定义菜单,需具有执行自定义菜单的权限
  118. @blueprint.route('/<int:app_id>/menus', methods=['GET'])
  119. @require_permission('publish_app_publish_menu_exec|publish_app_menu_view')
  120. def fetch_menus(app_id):
  121. q_type = request.args.get('type')
  122. if q_type == 'built-in':
  123. menus = DeployMenu.query.filter_by(app_id=app_id).all()
  124. built_in_menus = get_built_in_menus()
  125. for item in menus:
  126. built_in_menus[item.name]['command'] = item.command
  127. return json_response(list(built_in_menus.values()))
  128. pro = App.query.get_or_404(app_id)
  129. if q_type == 'all':
  130. menus = pro.menus[:]
  131. menus.extend(DeployMenu.query.filter_by(app_id=app_id).all()[:])
  132. else:
  133. menus = pro.menus
  134. return json_response(menus)
  135. # 用于编辑内置发布用菜单,需要具有发布菜单编辑权限
  136. @blueprint.route('/<int:app_id>/bind/menus', methods=['POST'])
  137. @require_permission('publish_app_menu_edit')
  138. def bind_menus(app_id):
  139. all_valid_menus = get_built_in_menus()
  140. form, error = JsonParser(
  141. Argument('name', filter=lambda x: x in all_valid_menus, help='无效的菜单名称!', default=''),
  142. Argument('command', default='')
  143. ).parse()
  144. if error is None:
  145. if form.name is '': # 可能通过发布页提供的添加预定义菜单的请求,格式[{name: 'x', desc: 'x', command: 'x'} ...]
  146. post_data = request.get_json()
  147. if isinstance(post_data, list) and all([isinstance(x, dict) and x['name'] in all_valid_menus for x in post_data]):
  148. for item in post_data:
  149. tmp = all_valid_menus[item['name']]
  150. tmp['command'] = item.get('command')
  151. tmp['app_id'] = app_id
  152. DeployMenu.upsert({'app_id': app_id, 'name': item['name']}, **tmp)
  153. return json_response()
  154. else:
  155. return json_response(message='错误的参数!')
  156. form.desc = get_built_in_menus()[form.name]['desc']
  157. form.app_id = app_id
  158. DeployMenu.upsert({'app_id': app_id, 'name': form.name}, **form)
  159. return json_response(message=error)