feat: Container mounted volumes support shared customization (#10457)

Refs  #8443
This commit is contained in:
ssongliu 2025-09-23 18:44:05 +08:00 committed by GitHub
parent 563ea985a4
commit 5c7d9b4ffe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 298 additions and 89 deletions

View file

@ -144,6 +144,7 @@ type VolumeHelper struct {
SourceDir string `json:"sourceDir"`
ContainerDir string `json:"containerDir"`
Mode string `json:"mode"`
Shared string `json:"shared"`
}
type PortHelper struct {
HostIP string `json:"hostIP"`

View file

@ -1509,6 +1509,9 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
Source: volume.SourceDir,
Target: volume.ContainerDir,
ReadOnly: volume.Mode == "ro",
BindOptions: &mount.BindOptions{
Propagation: mount.Propagation(volume.Shared),
},
})
config.Volumes[volume.ContainerDir] = struct{}{}
} else {
@ -1555,6 +1558,7 @@ func loadVolumeBinds(binds []container.MountPoint) []dto.VolumeHelper {
if bind.RW {
volumeItem.Mode = "rw"
}
volumeItem.Shared = string(bind.Propagation)
datas = append(datas, volumeItem)
}
return datas

View file

@ -848,6 +848,19 @@ const message = {
volumeHelper: 'Ensure that the content of the storage volume is correct',
modeRW: 'RW',
modeR: 'R',
sharedLabel: 'Propagation Mode',
private: 'Private',
privateHelper: 'Mount changes in the container and host do not affect each other',
rprivate: 'Recursive Private',
rprivateHelper: 'All mounts in the container are completely isolated from the host',
shared: 'Shared',
sharedHelper: 'Mount changes in the host and container are visible to each other',
rshared: 'Recursive Shared',
rsharedHelper: 'All mount changes in the host and container are visible to each other',
slave: 'Slave',
slaveHelper: 'The container can see host mount changes, but its own changes do not affect the host',
rslave: 'Recursive Slave',
rslaveHelper: 'All mounts in the container can see host changes, but do not affect the host',
mode: 'Mode',
env: 'Environments',
restartPolicy: 'Restart policy',

View file

@ -851,6 +851,20 @@ const message = {
volumeHelper: 'Asegúrese de que el contenido del volumen de almacenamiento sea correcto',
modeRW: 'RW',
modeR: 'R',
sharedLabel: 'Modo de Propagación',
private: 'Privado',
privateHelper: 'Los cambios de montaje en el contenedor y el host no se afectan mutuamente',
rprivate: 'Privado Recursivo',
rprivateHelper: 'Todos los montajes en el contenedor están completamente aislados del host',
shared: 'Compartido',
sharedHelper: 'Los cambios de montaje en el host y el contenedor son visibles entre ',
rshared: 'Compartido Recursivo',
rsharedHelper: 'Todos los cambios de montaje en el host y el contenedor son visibles entre ',
slave: 'Esclavo',
slaveHelper:
'El contenedor puede ver los cambios de montaje del host, pero sus propios cambios no afectan al host',
rslave: 'Esclavo Recursivo',
rslaveHelper: 'Todos los montajes en el contenedor pueden ver los cambios del host, pero no afectan al host',
mode: 'Modo',
env: 'Entornos',
restartPolicy: 'Política de reinicio',

View file

@ -825,6 +825,19 @@ const message = {
volumeHelper: 'ストレージボリュームのコンテンツが正しいことを確認してください',
modeRW: 'rw',
modeR: 'r',
sharedLabel: '伝播モード',
private: 'プライベート',
privateHelper: 'コンテナ内とホストのマウント変更は互いに干渉しません',
rprivate: '再帰的プライベート',
rprivateHelper: 'コンテナ内のすべてのマウントはホストから完全に隔離されています',
shared: '共有',
sharedHelper: 'ホストとコンテナ内のマウント変更は互いに表示されます',
rshared: '再帰的共有',
rsharedHelper: 'ホストとコンテナ内のすべてのマウント変更が互いに表示されます',
slave: 'スレーブ',
slaveHelper: 'コンテナはホストのマウント変更を確認できますが自身の変更はホストに影響しません',
rslave: '再帰的スレーブ',
rslaveHelper: 'コンテナ内のすべてのマウントはホストの変更を確認できますがホストに影響しません',
mode: 'モード',
env: '環境',
restartPolicy: 'ポリシーを再起動します',

View file

@ -819,6 +819,19 @@ const message = {
volumeHelper: '저장소 볼륨의 내용이 올바른지 확인하십시오.',
modeRW: '읽기/쓰기',
modeR: '읽기 전용',
sharedLabel: '전파 모드',
private: '비공개',
privateHelper: '컨테이너와 호스트의 마운트 변경 사항이 서로 영향을 주지 않음',
rprivate: '재귀적 비공개',
rprivateHelper: '컨테이너 모든 마운트가 호스트와 완전히 격리됨',
shared: '공유',
sharedHelper: '호스트와 컨테이너의 마운트 변경 사항이 서로 보임',
rshared: '재귀적 공유',
rsharedHelper: '호스트와 컨테이너의 모든 마운트 변경 사항이 서로 보임',
slave: '슬레이브',
slaveHelper: '컨테이너는 호스트 마운트 변경 사항을 있지만, 자신의 변경 사항은 호스트에 영향을 주지 않음',
rslave: '재귀적 슬레이브',
rslaveHelper: '컨테이너 모든 마운트가 호스트 변경 사항을 있지만 호스트에 영향을 주지 않음',
mode: '모드',
env: '환경',
restartPolicy: '재시작 정책',

View file

@ -841,6 +841,19 @@ const message = {
volumeHelper: 'Pastikan kandungan volum storan adalah betul',
modeRW: 'RW',
modeR: 'R',
sharedLabel: 'Mod Penyebaran',
private: 'Peribadi',
privateHelper: 'Perubahan pemasangan dalam bekas dan hos tidak saling mempengaruhi',
rprivate: 'Peribadi Rekursif',
rprivateHelper: 'Semua pemasangan dalam bekas diasingkan sepenuhnya dari hos',
shared: 'Berkongsi',
sharedHelper: 'Perubahan pemasangan dalam hos dan bekas kelihatan antara satu sama lain',
rshared: 'Berkongsi Rekursif',
rsharedHelper: 'Semua perubahan pemasangan dalam hos dan bekas kelihatan antara satu sama lain',
slave: 'Hamba',
slaveHelper: 'Bekas dapat melihat perubahan pemasangan hos, tetapi perubahan sendiri tidak mempengaruhi hos',
rslave: 'Hamba Rekursif',
rslaveHelper: 'Semua pemasangan dalam bekas dapat melihat perubahan hos, tetapi tidak mempengaruhi hos',
mode: 'Mod',
env: 'Persekitaran',
restartPolicy: 'Polisi Mulakan Semula',

View file

@ -837,6 +837,20 @@ const message = {
volumeHelper: 'Certifique-se de que o conteúdo do volume de armazenamento está correto',
modeRW: 'RW',
modeR: 'R',
sharedLabel: 'Modo de Propagação',
private: 'Privado',
privateHelper: 'As alterações de montagem no container e no host não se afetam mutuamente',
rprivate: 'Privado Recursivo',
rprivateHelper: 'Todas as montagens no container estão completamente isoladas do host',
shared: 'Compartilhado',
sharedHelper: 'As alterações de montagem no host e no container são visíveis entre si',
rshared: 'Compartilhado Recursivo',
rsharedHelper: 'Todas as alterações de montagem no host e no container são visíveis entre ',
slave: 'Escravo',
slaveHelper:
'O container pode ver as alterações de montagem do host, mas suas próprias alterações não afetam o host',
rslave: 'Escravo Recursivo',
rslaveHelper: 'Todas as montagens no container podem ver as alterações do host, mas não afetam o host',
mode: 'Modo',
env: 'Ambientes',
restartPolicy: 'Política de reinício',

View file

@ -837,6 +837,20 @@ const message = {
volumeHelper: 'Убедитесь, что содержимое тома хранения корректно',
modeRW: 'Чтение-Запись',
modeR: 'Только чтение',
sharedLabel: 'Режим Распространения',
private: 'Приватный',
privateHelper: 'Изменения монтирования в контейнере и хосте не влияют друг на друга',
rprivate: 'Рекурсивный Приватный',
rprivateHelper: 'Все монтирования в контейнере полностью изолированы от хоста',
shared: 'Общий',
sharedHelper: 'Изменения монтирования в хосте и контейнере видны друг другу',
rshared: 'Рекурсивный Общий',
rsharedHelper: 'Все изменения монтирования в хосте и контейнере видны друг другу',
slave: 'Подчиненный',
slaveHelper:
'Контейнер может видеть изменения монтирования хоста, но его собственные изменения не влияют на хост',
rslave: 'Рекурсивный Подчиненный',
rslaveHelper: 'Все монтирования в контейнере могут видеть изменения хоста, но не влияют на хост',
mode: 'Режим',
env: 'Переменные окружения',
restartPolicy: 'Политика перезапуска',

View file

@ -857,6 +857,19 @@ const message = {
volumeHelper: 'Depolama biriminin içeriğinin doğru olduğundan emin olun',
modeRW: 'RW',
modeR: 'R',
sharedLabel: 'Yayılma Modu',
private: 'Özel',
privateHelper: 'Konteyner ve hosttaki bağlama değişiklikleri birbirini etkilemez',
rprivate: 'Özyinelemeli Özel',
rprivateHelper: 'Konteynerdeki tüm bağlamalar hosttan tamamen izole edilmiştir',
shared: 'Paylaşılan',
sharedHelper: 'Host ve konteynerdeki bağlama değişiklikleri birbirine görünür',
rshared: 'Özyinelemeli Paylaşılan',
rsharedHelper: 'Host ve konteynerdeki tüm bağlama değişiklikleri birbirine görünür',
slave: 'Bağımlı',
slaveHelper: 'Konteyner host bağlama değişikliklerini görebilir, ancak kendi değişiklikleri hostu etkilemez',
rslave: 'Özyinelemeli Bağımlı',
rslaveHelper: 'Konteynerdeki tüm bağlamalar host değişikliklerini görebilir, ancak hostu etkilemez',
mode: 'Mod',
env: 'Ortamlar',
restartPolicy: 'Yeniden başlatma politikası',

View file

@ -813,6 +813,19 @@ const message = {
volumeHelper: '請確認磁碟區內容輸入正確',
modeRW: '讀寫',
modeR: '唯讀',
sharedLabel: '傳播模式',
private: '私有',
privateHelper: '容器裡的掛載變化和主機互不干擾',
rprivate: '遞歸私有',
rprivateHelper: '容器裡所有掛載都和主機完全隔離',
shared: '共享',
sharedHelper: '主機和容器裡的掛載變化互相可見',
rshared: '遞歸共享',
rsharedHelper: '主機和容器裡所有掛載變化都互相可見',
slave: '從屬',
slaveHelper: '容器能看見主機的掛載變化但自己的變化不影響主機',
rslave: '遞歸從屬',
rslaveHelper: '容器裡所有掛載都能看見主機變化但不影響主機',
mode: '權限',
env: '環境變數',
restartPolicy: '重啟規則',

View file

@ -812,6 +812,19 @@ const message = {
volumeHelper: '请确认存储卷内容输入正确',
modeRW: '读写',
modeR: '只读',
sharedLabel: '传播模式',
private: '私有',
privateHelper: '容器里的挂载变化和主机互不干扰',
rprivate: '递归私有',
rprivateHelper: '容器里所有挂载都和主机完全隔离',
shared: '共享',
sharedHelper: '主机和容器里的挂载变化互相可见',
rshared: '递归共享',
rsharedHelper: '主机和容器里所有挂载变化都互相可见',
slave: '从属',
slaveHelper: '容器能看见主机的挂载变化但自己的变化不影响主机',
rslave: '递归从属',
rslaveHelper: '容器里所有挂载都能看见主机变化但不影响主机',
mode: '权限',
env: '环境变量',
restartPolicy: '重启规则',

View file

@ -193,75 +193,7 @@
</el-tab-pane>
<el-tab-pane :label="$t('container.mount')">
<el-form-item>
<el-table v-if="form.volumes.length !== 0" :data="form.volumes">
<el-table-column :label="$t('container.server')" min-width="150">
<template #default="{ row }">
<el-radio-group v-model="row.type">
<el-radio-button value="volume">
{{ $t('container.volumeOption') }}
</el-radio-button>
<el-radio-button value="bind">
{{ $t('container.hostOption') }}
</el-radio-button>
</el-radio-group>
</template>
</el-table-column>
<el-table-column
:label="$t('container.volumeOption') + '/' + $t('container.hostOption')"
min-width="200"
>
<template #default="{ row }">
<el-select
v-if="row.type === 'volume'"
filterable
v-model="row.sourceDir"
>
<div v-for="(item, indexV) of volumes" :key="indexV">
<el-tooltip
:hide-after="20"
:content="item.option"
placement="top"
>
<el-option
:value="item.option"
:label="item.option.substring(0, 30)"
/>
</el-tooltip>
</div>
</el-select>
<el-input v-else v-model="row.sourceDir" />
</template>
</el-table-column>
<el-table-column :label="$t('container.mode')" min-width="130">
<template #default="{ row }">
<el-radio-group v-model="row.mode">
<el-radio value="rw">{{ $t('container.modeRW') }}</el-radio>
<el-radio value="ro">{{ $t('container.modeR') }}</el-radio>
</el-radio-group>
</template>
</el-table-column>
<el-table-column :label="$t('container.containerDir')" min-width="200">
<template #default="{ row }">
<el-input v-model="row.containerDir" />
</template>
</el-table-column>
<el-table-column min-width="80">
<template #default="scope">
<el-button
link
type="primary"
@click="handleVolumesDelete(scope.$index)"
>
{{ $t('commons.button.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-button class="ml-3 mt-2" @click="handleVolumesAdd()">
{{ $t('commons.button.add') }}
</el-button>
</el-form-item>
<Volume ref="volumeRef" :volumes="form.volumes"></Volume>
</el-tab-pane>
<el-tab-pane :label="$t('terminal.command')">
@ -439,9 +371,9 @@ import { Rules, checkFloatNumberRange, checkNumberRange } from '@/global/form-ru
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import Confirm from '@/views/container/container/operate/confirm.vue';
import Volume from '@/views/container/container/operate/volume.vue';
import {
listImage,
listVolume,
createContainer,
updateContainer,
loadResourceLimit,
@ -460,6 +392,7 @@ import { routerToName } from '@/utils/router';
const loading = ref(false);
const isCreate = ref();
const confirmRef = ref();
const volumeRef = ref();
const form = reactive<Container.ContainerHelper>({
taskID: '',
containerID: '',
@ -560,13 +493,11 @@ const search = async () => {
}
loadLimit();
loadImageOptions();
loadVolumeOptions();
loadNetworkOptions();
};
const taskLogRef = ref();
const images = ref();
const volumes = ref();
const networks = ref();
const limits = ref<Container.ResourceLimit>({
cpu: null as number,
@ -613,19 +544,6 @@ const goRouter = async () => {
routerToName('AppInstalled');
};
const handleVolumesAdd = () => {
let item = {
type: 'bind',
sourceDir: '',
containerDir: '',
mode: 'rw',
};
form.volumes.push(item);
};
const handleVolumesDelete = (index: number) => {
form.volumes.splice(index, 1);
};
const loadLimit = async () => {
const res = await loadResourceLimit();
limits.value = res.data;
@ -636,15 +554,12 @@ const loadImageOptions = async () => {
const res = await listImage();
images.value = res.data;
};
const loadVolumeOptions = async () => {
const res = await listVolume();
volumes.value = res.data;
};
const loadNetworkOptions = async () => {
const res = await listNetwork();
networks.value = res.data;
};
const onSubmit = async (formEl: FormInstance | undefined) => {
form.volumes = volumeRef.value.loadVolumes();
if (form.volumes.length !== 0) {
for (const item of form.volumes) {
if (!item.containerDir || !item.sourceDir) {

View file

@ -0,0 +1,156 @@
<template>
<div>
<ComplexTable stripe v-if="tmpVolumes" style="width: 100%" :data="tmpVolumes">
<el-table-column :label="$t('container.server')" min-width="200">
<template #default="{ row }">
<el-radio-group v-model="row.type">
<el-radio-button value="volume">
{{ $t('container.volumeOption') }}
</el-radio-button>
<el-radio-button value="bind">
{{ $t('container.hostOption') }}
</el-radio-button>
</el-radio-group>
</template>
</el-table-column>
<el-table-column :label="$t('container.server') + ' -> ' + $t('container.containerDir')" min-width="400">
<template #default="{ row }">
<el-select v-if="row.type === 'volume'" filterable v-model="row.sourceDir">
<div v-for="(item, indexV) of volumeOptions" :key="indexV">
<el-tooltip :hide-after="20" :content="item.option" placement="top">
<el-option :value="item.option" :label="item.option.substring(0, 30)" />
</el-tooltip>
</div>
</el-select>
<el-input v-else v-model="row.sourceDir" />
<el-icon><Bottom /></el-icon>
<el-input v-model="row.containerDir" />
</template>
</el-table-column>
<el-table-column :label="$t('container.mode')" min-width="180">
<template #default="{ row }">
<el-radio-group v-model="row.mode">
<el-radio value="rw">{{ $t('container.modeRW') }}</el-radio>
<el-radio value="ro">{{ $t('container.modeR') }}</el-radio>
</el-radio-group>
</template>
</el-table-column>
<el-table-column :label="$t('container.sharedLabel')" min-width="180">
<template #default="{ row }">
<el-select popper-class="tall-options" v-model="row.shared">
<el-option value="private" :label="$t('container.private')">
<div class="title">{{ $t('container.private') }}</div>
<div class="description">{{ $t('container.privateHelper') }}</div>
</el-option>
<el-option value="rprivate" :label="$t('container.rprivate')">
<div class="title">{{ $t('container.rprivate') }}</div>
<div class="description">{{ $t('container.rprivateHelper') }}</div>
</el-option>
<el-option value="shared" :label="$t('container.shared')">
<div class="title">{{ $t('container.shared') }}</div>
<div class="description">{{ $t('container.sharedHelper') }}</div>
</el-option>
<el-option value="rshared" :label="$t('container.rshared')">
<div class="title">{{ $t('container.rshared') }}</div>
<div class="description">{{ $t('container.rsharedHelper') }}</div>
</el-option>
<el-option value="slave" :label="$t('container.slave')">
<div class="title">{{ $t('container.slave') }}</div>
<div class="description">{{ $t('container.slaveHelper') }}</div>
</el-option>
<el-option value="rslave" :label="$t('container.rslave')">
<div class="title">{{ $t('container.rslave') }}</div>
<div class="description">{{ $t('container.rslaveHelper') }}</div>
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column min-width="80" :fixed="'right'">
<template #default="scope">
<el-button link type="primary" @click="handleVolumesDelete(scope.$index)">
{{ $t('commons.button.delete') }}
</el-button>
</template>
</el-table-column>
</ComplexTable>
<el-button class="mt-2" @click="handleVolumesAdd()">
{{ $t('commons.button.add') }}
</el-button>
</div>
</template>
<script lang="ts" setup>
import { Container } from '@/api/interface/container';
import { listVolume } from '@/api/modules/container';
import { ref, onMounted } from 'vue';
const tmpVolumes = ref([]);
const volumeOptions = ref();
const props = defineProps({
volumes: { type: Array<Container.Volume>, default: [] },
});
watch(
() => props.volumes,
(newVal) => {
console.log(newVal);
tmpVolumes.value = newVal || [];
},
);
const loadVolumeOptions = async () => {
const res = await listVolume();
volumeOptions.value = res.data;
};
const handleVolumesAdd = () => {
let item = {
type: 'volume',
sourceDir: '',
containerDir: '',
mode: 'rw',
shared: 'private',
};
tmpVolumes.value.push(item);
};
const handleVolumesDelete = (index: number) => {
tmpVolumes.value.splice(index, 1);
};
const loadVolumes = () => {
return tmpVolumes.value;
};
onMounted(() => {
tmpVolumes.value = props.volumes || [];
loadVolumeOptions();
});
defineExpose({
loadVolumes,
});
</script>
<style lang="scss" scoped>
.tall-options {
.el-select-dropdown__item {
height: 50px !important;
padding: 5px 15px !important;
line-height: 1.5 !important;
font-size: 14px;
&.selected {
font-weight: bold;
}
&:hover {
background-color: #f0f9ff;
}
}
.title {
font-size: 14px;
font-weight: 500;
}
.description {
font-size: 12px;
color: #adb0bc;
}
}
</style>