diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 0364d07..dce7a2b 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -11,7 +11,7 @@ permissions: env: PYTHON_VERSION: '3.9.13' - RELEASE_VERSION: 'v1.1.3' # 发布版本 + RELEASE_VERSION: 'v1.1.4' # 发布版本 DOCKER_IMAGE_NAME: 'udocker' DOCKER_NAMESPACE: 'llody' DOCKER_REGISTRY_HUAWEI: 'swr.cn-southwest-2.myhuaweicloud.com' @@ -116,10 +116,8 @@ jobs: body: | - **新版本发布**: ${{ env.RELEASE_VERSION }} - **更新内容**: - - * 镜像仓库-新增备注信息。 - - * BUG - 优化镜像过多时加载慢的情况。 - - * 系统信息 - 新增主机使用率展示。 - - * 镜像仓库-新增dockerhub代理,(docker.llody.cn)。 + - * 容器管理 - 新增镜像回滚功能,可以对容器正在使用的镜像进行镜像回滚,切换镜像并重新创建容器。 + - * 镜像仓库 - 新增dockerhub代理,(docker.llody.cn)。 - * 页面优化。 - * 流水线优化 - 新增同步国内镜像仓库镜像。 draft: false diff --git a/README.md b/README.md index 70c62a1..8c0deab 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ **如果此项目对你有用,请给一个**:star: ## 资源 -> 只需要:1核1G即可,暂时只有X86版本 +> 1核1G即可轻松运行,支持只有 **X86** 和**arm64**架构 ## 快速了解 > udocker 是一个轻量且好用的docker管理面板,并且自带一个webssh终端管理工具,可以很方便的管理服务器和上传下载文件。 @@ -22,6 +22,7 @@ - 镜像管理 - 容器管理 + - * 镜像回滚:可以对容器正在使用的镜像进行回滚版本,建议使用 **latest** 版本号时使用。 - 网络管理 - 存储管理 - 事件中心 diff --git a/apps/urls.py b/apps/urls.py index f3ba65b..8cefb67 100644 --- a/apps/urls.py +++ b/apps/urls.py @@ -24,6 +24,7 @@ re_path("docker_image_info/",views.docker_image_info,name="docker_image_info"), # 远程docker镜像管理 re_path("docker_images_api/",views.docker_images_api,name="docker_images_api"), # 远程docker镜像管理API re_path("docker_images_pull/",views.docker_images_pull,name="docker_images_pull"), # 远程docker镜像管理拉取镜像 + re_path("docker_rollback_api/",views.docker_rollback_api,name="docker_rollback_api"), re_path("get_images_list/",views.get_images_list,name="get_images_list"), re_path("get_registries_list/",views.get_registries_list,name="get_registries_list"), diff --git a/apps/views.py b/apps/views.py index 8110784..dea6095 100644 --- a/apps/views.py +++ b/apps/views.py @@ -846,7 +846,7 @@ def docker_images_api(request): # 获取所有容器的镜像ID container_image_ids = {container.image.id for container in containers} def process_image(image): - image_id = image.id + image_id = image.short_id image_tags = image.tags image_size = humanize.naturalsize(image.attrs['Size'], binary=True) time_str = image.attrs['Created'] @@ -1140,7 +1140,7 @@ def get_volumes_list(request): def get_historicalmirror_list(request): mirror_data = [] image_info = request.GET.get("image") - print("传递的镜像:",image_info) + container_info = request.GET.get("name") try: #容器管理模块API success, client = docker_mod.connect_to_docker() @@ -1161,19 +1161,140 @@ def get_historicalmirror_list(request): historical_mirror_data = [] for image in specific_images: tag_version = image.tags[0].split(':')[-1] if image.tags else '' + created_time = parser.isoparse(image.attrs['Created']) + formatted_created_time = created_time.strftime('%Y-%m-%d %H:%M:%S') + size_mb = round(image.attrs['VirtualSize'] / 1024 / 1024) historical_mirror_data.append({ + 'container_info':container_info, + 'current_mirror': image_info, 'REPOSITORY': image.tags[0] if image.tags else '', 'TAG': tag_version, - 'IMAGE ID': image.short_id, - 'CREATED': image.attrs['Created'], - 'SIZE': image.attrs['VirtualSize']/1024/1024, + 'IMAGE_ID': image.short_id, + 'CREATED': formatted_created_time, + 'SIZE': size_mb, }) - print("数据:",historical_mirror_data) return render(request, 'container/docker_mirror.html',{"historical_mirror_data":historical_mirror_data}) except DockerException as e: logger.error(e) +# 回滚方法 +@csrf_exempt +@login_required +def docker_rollback_api(request): + if request.method == 'POST': + request_data = QueryDict(request.body) + rollback_name = request_data.get("container_info") + rollback_image = request_data.get("rollbackImageId") + print(f"回滚容器名称:{rollback_name},回滚镜像ID:{rollback_image}") + try: + #容器管理模块API + success, client = docker_mod.connect_to_docker() + if not success: + return JsonResponse({'code': 1, 'msg': '无法连接到Docker'}) + # 获取指定容器 + container = client.containers.get(rollback_name) + # 获取当前容器正在使用的镜像 + current_image_name = container.attrs['Config']['Image'] + # 记录回滚前的镜像ID,为报错回滚事务做准备 + original_image_id = container.attrs['Image'] + + # 备份原始容器配置 + original_config = { + 'Config': container.attrs['Config'], + 'HostConfig': container.attrs['HostConfig'], + 'NetworkSettings': container.attrs['NetworkSettings'] + } + + # 重新打TAG + client.images.get(rollback_image).tag(current_image_name) + + # 提取原容器的配置信息 + config = container.attrs['Config'] + network_config = container.attrs['NetworkSettings'] + + # 生成新的host_config + host_config = client.api.create_host_config( + binds=container.attrs['HostConfig'].get('Binds'), + port_bindings=container.attrs['HostConfig'].get('PortBindings'), + links=container.attrs['HostConfig'].get('Links'), + publish_all_ports=container.attrs['HostConfig'].get('PublishAllPorts', False), + privileged=container.attrs['HostConfig'].get('Privileged', False), + dns=container.attrs['HostConfig'].get('Dns'), + dns_search=container.attrs['HostConfig'].get('DnsSearch'), + volumes_from=container.attrs['HostConfig'].get('VolumesFrom'), + network_mode=container.attrs['HostConfig'].get('NetworkMode'), + restart_policy=container.attrs['HostConfig'].get('RestartPolicy'), + cap_add=container.attrs['HostConfig'].get('CapAdd'), + cap_drop=container.attrs['HostConfig'].get('CapDrop'), + devices=container.attrs['HostConfig'].get('Devices'), + ulimits=container.attrs['HostConfig'].get('Ulimits'), + log_config=container.attrs['HostConfig'].get('LogConfig'), + ) + # 检查网络配置 + if len(network_config['Networks']) > 1: + result = {'code': 1, 'msg': '原容器连接到多个网络,请手动处理网络配置'} + return JsonResponse(result) + + # 停止并删除当前容器 + container.stop() + container.remove() + + # 生成新的端口映射 + ports = {} + exposed_ports = {} + if 'Ports' in network_config: + for port, mappings in network_config['Ports'].items(): + if mappings is not None: + for mapping in mappings: + ports[port] = mapping['HostPort'] + exposed_ports[port] = {} + + # 重新创建容器 + new_container = client.api.create_container( + image=current_image_name, + name=rollback_name, + command=config['Cmd'], + environment=config.get('Env', []), # 确保Env存在 + host_config=host_config, + networking_config=client.api.create_networking_config({ + list(network_config['Networks'].keys())[0]: network_config['Networks'][list(network_config['Networks'].keys())[0]] + }), + volumes=config['Volumes'], + ports=exposed_ports + ) + + # 启动新容器 + client.api.start(new_container['Id']) + + result = {'code': 0, 'msg': '回滚成功,可返回容器管理查看!'} + return JsonResponse(result) + except DockerException as e: + logger.error(e) + # 回滚镜像tag到原始状态 + try: + client.images.get(current_image_name).tag(original_image_id) + print(f"镜像TAG因容器创建失败进行回滚!!!") + except Exception as rollback_err: + print(f"镜像回滚失败 tag: {rollback_err}") + + # 恢复原始容器 + try: + if 'Config' in original_config and 'HostConfig' in original_config and 'NetworkSettings' in original_config: + new_container = client.containers.create( + image=original_config['Config']['Image'], + name=rollback_name, + command=original_config['Config']['Cmd'], + environment=original_config['Config'].get('Env', []), + host_config=original_config['HostConfig'], + networking_config=original_config['NetworkSettings'] + ) + client.api.start(new_container.id) + except Exception as restore_err: + print(f"Error restoring container: {restore_err}") + + result = {'code': 1, 'msg': str(e)} + return JsonResponse(result) # 网络管理 @login_required def docker_network_info(request): diff --git a/templates/container/docker_container_list.html b/templates/container/docker_container_list.html index 3b5aa25..0816458 100644 --- a/templates/container/docker_container_list.html +++ b/templates/container/docker_container_list.html @@ -622,23 +622,27 @@ var checkStatus = table.checkStatus(obj.config.id) var data = checkStatus.data; if(data.length == 0 ) { - layer.msg("请至少选择一行") + layer.msg("请至少选择一行", { icon: 5 }) } else if (data.length > 1){ - layer.msg("不支持多选") + layer.msg("不支持多选", { icon: 5 }) } else { - layer.open({ - type: 2, - area: ['50%', '70%'], - closeBtn: true, // 1或者2表示开启关闭按钮,0表示不开启 - title: "文件管理器", - shade: 0.1, - shift: 2, - shadeClose: false, - content: '{% url "get_historicalmirror_list" %}?image=' + data[0].image, - move: false, // 禁止拖动 - resize: false, // 禁止调整大小 - skin: 'white-background' // 应用自定义的背景颜色类 - }); + layer.confirm('镜像回滚会重新创建容器,请测试确认要这么做?
请注意,此操作不可逆', function (index) { + layer.open({ + type: 2, + area: ['50%', '70%'], + closeBtn: true, // 1或者2表示开启关闭按钮,0表示不开启 + title: "容器镜像回滚", + shade: 0.1, + shift: 2, + shadeClose: false, + content: '{% url "get_historicalmirror_list" %}?image=' + data[0].image + '&name=' + data[0].name, + move: false, // 禁止拖动 + resize: false, // 禁止调整大小 + skin: 'white-background' // 应用自定义的背景颜色类 + }); + layer.close(index); + }) + } break } diff --git a/templates/container/docker_mirror.html b/templates/container/docker_mirror.html index a15a00b..2f387ab 100644 --- a/templates/container/docker_mirror.html +++ b/templates/container/docker_mirror.html @@ -14,6 +14,7 @@ + {% csrf_token %}
@@ -33,17 +34,31 @@ - REPOSITORY - TAG - CREATED - SIZE + 仓库 + 版本 + 镜像ID + 创建时间 + 大小 + 状态 + 操作 {% for mirror in historical_mirror_data %} {{ mirror.REPOSITORY }} {{ mirror.TAG }} + {{ mirror.IMAGE_ID }} {{ mirror.CREATED }} - {{ mirror.SIZE }} + {{ mirror.SIZE }} MB + + {% if mirror.REPOSITORY == mirror.current_mirror %} + UP + {% else %} + - + {% endif %} + + + + {% endfor %} @@ -55,5 +70,48 @@
+