fix: Optimize container image repo restart detection logic (#9447)

This commit is contained in:
ssongliu 2025-07-08 10:47:53 +08:00 committed by GitHub
parent b1adc37920
commit 29a304e2c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 127 additions and 70 deletions

View file

@ -76,7 +76,7 @@ func (b *BaseApi) CheckRepoStatus(c *gin.Context) {
// @Tags Container Image-repo
// @Summary Create image repo
// @Accept json
// @Param request body dto.ImageRepoDelete true "request"
// @Param request body dto.ImageRepoCreate true "request"
// @Produce json
// @Success 200
// @Security ApiKeyAuth
@ -99,20 +99,20 @@ func (b *BaseApi) CreateRepo(c *gin.Context) {
// @Tags Container Image-repo
// @Summary Delete image repo
// @Accept json
// @Param request body dto.ImageRepoDelete true "request"
// @Param request body dto.OperateByID true "request"
// @Produce json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/repo/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"image_repos","output_column":"name","output_value":"names"}],"formatZH":"删除镜像仓库 [names]","formatEN":"delete image repo [names]"}
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"image_repos","output_column":"name","output_value":"name"}],"formatZH":"删除镜像仓库 [name]","formatEN":"delete image repo [name]"}
func (b *BaseApi) DeleteRepo(c *gin.Context) {
var req dto.ImageRepoDelete
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := imageRepoService.BatchDelete(req); err != nil {
if err := imageRepoService.Delete(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}

View file

@ -14,7 +14,6 @@ import (
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
@ -27,7 +26,7 @@ type IImageRepoService interface {
Login(req dto.OperateByID) error
Create(req dto.ImageRepoCreate) error
Update(req dto.ImageRepoUpdate) error
BatchDelete(req dto.ImageRepoDelete) error
Delete(req dto.OperateByID) error
}
func NewIImageRepoService() IImageRepoService {
@ -90,30 +89,7 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
}
if err := validateDockerConfig(); err != nil {
return err
}
if err := restartDocker(); err != nil {
return err
}
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
if err := func() error {
for range ticker.C {
select {
case <-ctx.Done():
cancel()
return errors.New("the docker service cannot be restarted")
default:
if ok, _ := systemctl.IsActive("docker"); ok {
global.LOG.Info("docker restart with new conf successful!")
return nil
}
}
}
return nil
}(); err != nil {
if err := stopBeforeUpdateRepo(); err != nil {
return err
}
}
@ -131,15 +107,32 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
return imageRepoRepo.Create(&imageRepo)
}
func (u *ImageRepoService) BatchDelete(req dto.ImageRepoDelete) error {
for _, id := range req.Ids {
if id == 1 {
return errors.New("The default value cannot be edit !")
}
func (u *ImageRepoService) Delete(req dto.OperateByID) error {
if req.ID == 1 {
return errors.New("The default value cannot be edit !")
}
if err := imageRepoRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil {
itemRepo, _ := imageRepoRepo.Get(commonRepo.WithByID(req.ID))
if itemRepo.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
if itemRepo.Auth {
_, _ = cmd.Execf("docker logout -i %s", itemRepo.DownloadUrl)
}
if itemRepo.Protocol == "https" {
return imageRepoRepo.Delete(commonRepo.WithByID(req.ID))
}
if err := u.handleRegistries("", itemRepo.DownloadUrl, "delete"); err != nil {
return fmt.Errorf("delete registry %s failed, err: %v", itemRepo.DownloadUrl, err)
}
if err := validateDockerConfig(); err != nil {
return err
}
if err := imageRepoRepo.Delete(commonRepo.WithByID(req.ID)); err != nil {
return err
}
go func() {
_ = restartDocker()
}()
return nil
}
@ -154,37 +147,37 @@ func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error {
if err != nil {
return err
}
needRestart := false
if repo.Protocol == "http" && req.Protocol == "https" {
if err := u.handleRegistries("", repo.DownloadUrl, "delete"); err != nil {
return fmt.Errorf("delete registry %s failed, err: %v", repo.DownloadUrl, err)
}
needRestart = true
}
if repo.Protocol == "http" && req.Protocol == "http" {
if err := u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update"); err != nil {
return fmt.Errorf("update registry %s => %s failed, err: %v", repo.DownloadUrl, req.DownloadUrl, err)
}
needRestart = repo.DownloadUrl == req.DownloadUrl
}
if repo.Protocol == "https" && req.Protocol == "http" {
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
}
needRestart = true
}
if repo.Auth != req.Auth || repo.DownloadUrl != req.DownloadUrl {
if repo.Auth {
_, _ = cmd.ExecWithCheck("docker", "logout", repo.DownloadUrl)
}
if req.Auth {
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
return err
}
if needRestart {
if err := stopBeforeUpdateRepo(); err != nil {
return err
}
}
if err := validateDockerConfig(); err != nil {
return err
if repo.Auth {
_, _ = cmd.Execf("docker logout -i %s", repo.DownloadUrl)
}
if err := restartDocker(); err != nil {
return err
if req.Auth {
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
return err
}
}
upMap := make(map[string]interface{})
@ -256,3 +249,34 @@ func (u *ImageRepoService) handleRegistries(newHost, delHost, handle string) err
}
return nil
}
func stopBeforeUpdateRepo() error {
if err := validateDockerConfig(); err != nil {
return err
}
if err := restartDocker(); err != nil {
return err
}
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
if err := func() error {
for range ticker.C {
select {
case <-ctx.Done():
cancel()
return errors.New("the docker service cannot be restarted")
default:
stdout, err := cmd.Exec("systemctl is-active docker")
if string(stdout) == "active\n" && err == nil {
global.LOG.Info("docker restart with new conf successful!")
return nil
}
}
}
return nil
}(); err != nil {
return err
}
return nil
}

View file

@ -212,9 +212,6 @@ export namespace Container {
password: string;
auth: boolean;
}
export interface RepoDelete {
ids: Array<number>;
}
export interface RepoInfo {
id: number;
createdAt: Date;

View file

@ -135,8 +135,8 @@ export const createImageRepo = (params: Container.RepoCreate) => {
export const updateImageRepo = (params: Container.RepoUpdate) => {
return http.post(`/containers/repo/update`, params, TimeoutEnum.T_40S);
};
export const deleteImageRepo = (params: Container.RepoDelete) => {
return http.post(`/containers/repo/del`, params, TimeoutEnum.T_40S);
export const deleteImageRepo = (id: Number) => {
return http.post(`/containers/repo/del`, { id: id }, TimeoutEnum.T_40S);
};
// composeTemplate

View file

@ -833,7 +833,7 @@ const message = {
repo: 'Registries',
createRepo: 'Add',
createRepoHelper: 'Adding an HTTP-type repository requires restarting the Docker service.',
httpRepoHelper: 'Operating an HTTP-type repository requires restarting the Docker service.',
httpRepo: 'Choosing HTTP protocol requires restarting the Docker service to add it into insecure registries.',
delInsecure: 'Deletion of credit',
delInsecureHelper:

View file

@ -829,7 +829,7 @@ const message = {
repo: 'レジストリ',
createRepo: '追加',
createRepoHelper: 'HTTPタイプのリポジトリを追加するにはDockerサービスの再起動が必要です',
httpRepoHelper: 'HTTPタイプのリポジトリを操作するにはDockerサービスの再起動が必要です',
httpRepo:
'HTTPプロトコルを選択するにはDockerサービスを再起動して不安定なレジストリに追加する必要があります',
delInsecure: 'クレジットの削除',

View file

@ -819,7 +819,7 @@ const message = {
repo: '레지스트리',
createRepo: '추가',
createRepoHelper: 'HTTP 타입 저장소 추가 Docker 서비스 재시작이 필요합니다.',
httpRepoHelper: 'HTTP 타입 저장소 작업 Docker 서비스 재시작이 필요합니다.',
httpRepo: 'HTTP 프로토콜을 선택하면 Docker 서비스를 재시작하여 불안정한 레지스트리에 추가해야 합니다.',
delInsecure: '신뢰할 없는 항목 삭제',
delInsecureHelper:

View file

@ -845,7 +845,7 @@ const message = {
repo: 'Pendaftaran',
createRepo: 'Tambah',
createRepoHelper: 'Menambah repositori jenis HTTP memerlukan mulakan semula perkhidmatan Docker.',
httpRepoHelper: 'Mengoperasikan repositori jenis HTTP memerlukan mulakan semula perkhidmatan Docker.',
httpRepo:
'Memilih protokol HTTP memerlukan memulakan semula perkhidmatan Docker untuk menambahkannya ke pendaftaran tidak selamat.',
delInsecure: 'Padamkan pendaftaran tidak selamat',

View file

@ -841,7 +841,7 @@ const message = {
repo: 'Registries',
createRepo: 'Adicionar',
createRepoHelper: 'Adicionar um repositório do tipo HTTP requer reinicialização do serviço Docker.',
httpRepoHelper: 'Operar um repositório do tipo HTTP requer reinicialização do serviço Docker.',
httpRepo:
'Escolher o protocolo HTTP requer reiniciar o serviço Docker para adicioná-lo a registries inseguros.',
delInsecure: 'Remover da lista de segurança',

View file

@ -843,7 +843,7 @@ const message = {
repo: 'Реестры',
createRepo: 'Добавить',
createRepoHelper: 'Добавление репозитория HTTP-типа требует перезапуска службы Docker.',
httpRepoHelper: 'Работа с репозиторием HTTP-типа требует перезапуска службы Docker.',
httpRepo: 'Выбор HTTP протокола требует перезапуска службы Docker для добавления в небезопасные реестры.',
delInsecure: 'Удаление учетных данных',
delInsecureHelper: 'Это перезапустит службу Docker для удаления из небезопасных реестров. Хотите продолжить?',

View file

@ -804,7 +804,7 @@ const message = {
repo: '倉庫',
createRepo: '新增倉庫',
createRepoHelper: '添加 HTTP 類型倉庫需要重啟 Docker 服務',
httpRepoHelper: '操作 HTTP 類型倉庫需要重啟 Docker 服務',
downloadUrl: '下載網址',
imageRepo: '鏡像倉庫',
repoHelper: '是否包含鏡像倉庫/組織/項目?',

View file

@ -806,7 +806,7 @@ const message = {
repo: '仓库',
createRepo: '添加仓库',
createRepoHelper: '添加 http 类型仓库需要重启 Docker 服务',
httpRepoHelper: '操作 http 类型仓库需要重启 Docker 服务',
downloadUrl: '下载地址',
imageRepo: '镜像仓库',
repoHelper: '是否包含镜像仓库/组织/项目?',

View file

@ -61,8 +61,9 @@
</template>
</LayoutContent>
<OpDialog ref="opRef" @search="search" />
<OpDialog ref="opRef" @search="search" @submit="submitDelete" />
<OperatorDialog @search="search" ref="dialogRef" />
<ConfirmDialog ref="confirmDialog" @confirm="submitDelete" />
</div>
</template>
@ -87,6 +88,8 @@ const paginationConfig = reactive({
const searchName = ref();
const opRef = ref();
const confirmDialog = ref();
const currentRepo = ref();
const dockerStatus = ref('Running');
const loadStatus = async () => {
@ -141,7 +144,16 @@ const onOpenDialog = async (
};
const onDelete = async (row: Container.RepoInfo) => {
let ids = [row.id];
currentRepo.value = row.id;
if (row.protocol === 'http') {
let params = {
header: i18n.global.t('container.repo'),
operationInfo: i18n.global.t('container.httpRepoHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialog.value!.acceptParams(params);
return;
}
opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
names: [row.name],
@ -149,11 +161,23 @@ const onDelete = async (row: Container.RepoInfo) => {
i18n.global.t('container.repo'),
i18n.global.t('commons.button.delete'),
]),
api: deleteImageRepo,
params: { ids: ids },
api: null,
params: null,
});
};
const submitDelete = async () => {
loading.value = true;
await deleteImageRepo(currentRepo.value)
.then(() => {
loading.value = false;
search();
})
.catch(() => {
loading.value = false;
});
};
const onCheckConn = async (row: Container.RepoInfo) => {
loading.value = true;
await checkRepoStatus(row.id)

View file

@ -108,6 +108,8 @@ const dialogData = ref<DialogProps>({
title: '',
});
const confirmDialog = ref();
const oldUrl = ref();
const oldProto = ref();
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
@ -147,13 +149,23 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (dialogData.value.rowData.protocol === 'https') {
let newProto = dialogData.value.rowData.protocol;
if (newProto === 'https' && dialogData.value.title === 'add') {
submit();
return;
}
if (newProto === oldProto.value) {
if (
(oldProto.value === 'http' && dialogData.value.rowData.downloadUrl === oldUrl.value) ||
oldProto.value === 'https'
) {
submit();
return;
}
}
let params = {
header: i18n.global.t('container.imageRepo'),
operationInfo: i18n.global.t('container.createRepoHelper'),
header: i18n.global.t('container.repo'),
operationInfo: i18n.global.t('container.httpRepoHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialog.value!.acceptParams(params);