mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-04 21:26:08 +08:00
Compare commits
54
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
535a6067c9 | ||
|
|
ace28d1c0e | ||
|
|
1df5af32b8 | ||
|
|
fbd4a1108f | ||
|
|
823a88c85b | ||
|
|
3a2c6e3a3d | ||
|
|
be6e9b5f06 | ||
|
|
a88bed9eb4 | ||
|
|
f97f14c811 | ||
|
|
f8a29a1c54 | ||
|
|
9a935434c2 | ||
|
|
afe632b723 | ||
|
|
d050f21f9a | ||
|
|
b87f367583 | ||
|
|
92aa8ec603 | ||
|
|
e013534373 | ||
|
|
d23c3d3b72 | ||
|
|
719c33fa12 | ||
|
|
a6bd7ec00e | ||
|
|
ac26fe2f0e | ||
|
|
790cade04d | ||
|
|
2903e86c02 | ||
|
|
2d747e435a | ||
|
|
eadda2a18b | ||
|
|
a178302f73 | ||
|
|
d376bfb3a5 | ||
|
|
7a84e08f99 | ||
|
|
3d39626b93 | ||
|
|
1c3b40b631 | ||
|
|
87baf75301 | ||
|
|
a10d651827 | ||
|
|
11ff7c8a7c | ||
|
|
f6dffe9816 | ||
|
|
b813bc578e | ||
|
|
7c3b2730ab | ||
|
|
429d9e18ad | ||
|
|
dc166d8fc9 | ||
|
|
3ff4534294 | ||
|
|
d4223ad2ae | ||
|
|
44297ada16 | ||
|
|
aea53fdd65 | ||
|
|
9640c87947 | ||
|
|
2cefd7cb78 | ||
|
|
fb6ffa3c0f | ||
|
|
e0436bff28 | ||
|
|
219f39ae52 | ||
|
|
41cf44d231 | ||
|
|
7d09cb7142 | ||
|
|
560643d299 | ||
|
|
c3bb0ea4be | ||
|
|
3c37f04ef6 | ||
|
|
d98db6ba49 | ||
|
|
531ed03796 | ||
|
|
59f260eab8 |
@@ -8,12 +8,16 @@
|
|||||||
- [ ] Yes, the backend owner is @
|
- [ ] Yes, the backend owner is @
|
||||||
- [ ] No, frontend-only.
|
- [ ] No, frontend-only.
|
||||||
|
|
||||||
### Related Issue #
|
### Related Issue
|
||||||
<!-- Link the issue as harvester/harvester#<issue_number> -->
|
<!-- Link the issue as harvester/harvester#<issue_number> -->
|
||||||
harvester/harvester#<issue_number>
|
harvester/harvester#<issue_number>
|
||||||
|
|
||||||
|
|
||||||
### Test screenshot or video (Required)
|
### Test Steps
|
||||||
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
<!-- Provide the test steps for reviewer to verify the issue -->
|
||||||
|
1. Go to ... page
|
||||||
|
|
||||||
|
### Test screenshot or video
|
||||||
|
<!-- Attach screenshot or video of the changes and eventual comparison -->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
name: Build Standalone on PR Merge
|
name: Build Standalone on PR Merge
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch: # manually trigger on Github Actions only support PR branch pushed to harvester/harvester-ui-extension
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -29,3 +30,35 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
with:
|
with:
|
||||||
CI_BRANCH: ${{github.ref_name}}
|
CI_BRANCH: ${{github.ref_name}}
|
||||||
|
comment-on-pr:
|
||||||
|
name: Comment build result on PR
|
||||||
|
# Only for manual runs so the existing push / pull_request flows are untouched.
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Add build artifact comment to the PR opened from this branch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BRANCH: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Match scripts/version: sanitize the branch and map main -> latest.
|
||||||
|
if [ "$BRANCH" = "main" ]; then
|
||||||
|
DIR="latest"
|
||||||
|
else
|
||||||
|
DIR=$(printf '%s' "$BRANCH" | sed -E 's/[^a-zA-Z0-9.-]+/-/g')
|
||||||
|
fi
|
||||||
|
URL="https://releases.rancher.com/harvester-ui/dashboard/${DIR}/index.html"
|
||||||
|
|
||||||
|
PR=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --base main --head "$BRANCH" --json number --jq '.[0].number // empty')
|
||||||
|
if [ -z "$PR" ]; then
|
||||||
|
echo "No open PR found for branch $BRANCH, skipping comment."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY=$(printf '🚀 **UI Artifact Build Status:** `SUCCESS` ✅\n> ui-index: %s\n> ui-source: external' "$URL")
|
||||||
|
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --body "$BODY"
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.9.0-rc7",
|
"version": "1.10.0-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"glob-parent": "6.0.2",
|
"glob-parent": "6.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"@types/lodash": "4.17.25",
|
"@types/lodash": "4.17.24",
|
||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"node-forge": "1.4.0",
|
"node-forge": "1.4.0",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
|
|||||||
@@ -231,21 +231,14 @@ export default {
|
|||||||
v-if="setting.customized"
|
v-if="setting.customized"
|
||||||
class="modified"
|
class="modified"
|
||||||
>
|
>
|
||||||
{{ t('advancedSettings.tags.modified.label') }}
|
Modified
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="setting.experimental"
|
v-if="setting.experimental"
|
||||||
v-clean-tooltip="t('advancedSettings.tags.experimental.description')"
|
v-clean-tooltip="t('advancedSettings.experimental')"
|
||||||
class="experimental"
|
class="experimental"
|
||||||
>
|
>
|
||||||
{{ t('advancedSettings.tags.experimental.label') }}
|
Experimental
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="setting.preview"
|
|
||||||
v-clean-tooltip="t('advancedSettings.tags.preview.description')"
|
|
||||||
class="preview"
|
|
||||||
>
|
|
||||||
{{ t('advancedSettings.tags.preview.label') }}
|
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
||||||
@@ -359,19 +352,21 @@ export default {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modified,
|
.modified {
|
||||||
.experimental,
|
|
||||||
.preview {
|
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
border: 1px solid;
|
border: 1px solid var(--primary);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modified { border-color: var(--primary); }
|
.experimental {
|
||||||
.experimental { border-color: var(--error); }
|
margin-left: 10px;
|
||||||
.preview { border-color: var(--warning); }
|
border: 1px solid var(--error);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.no-search-match {
|
.no-search-match {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import MappingColumn from './MappingColumn.vue';
|
|||||||
import StorageDefaultsModal from './StorageDefaultsModal.vue';
|
import StorageDefaultsModal from './StorageDefaultsModal.vue';
|
||||||
|
|
||||||
const DEFAULT_VOLUME_MODE = VOLUME_MODE.FILE_SYSTEM;
|
const DEFAULT_VOLUME_MODE = VOLUME_MODE.FILE_SYSTEM;
|
||||||
const DEFAULT_ACCESS_MODE = ACCESS_MODE?.READ_WRITE_MANY ?? 'ReadWriteMany';
|
const DEFAULT_ACCESS_MODES = [ACCESS_MODE.READ_WRITE_MANY];
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
providerName: { type: String, default: '' },
|
providerName: { type: String, default: '' },
|
||||||
@@ -268,9 +268,9 @@ const buildStorageEntries = () => {
|
|||||||
capacity: 0,
|
capacity: 0,
|
||||||
target: '',
|
target: '',
|
||||||
volumeMode: DEFAULT_VOLUME_MODE,
|
volumeMode: DEFAULT_VOLUME_MODE,
|
||||||
accessModes: [DEFAULT_ACCESS_MODE],
|
accessModes: [...DEFAULT_ACCESS_MODES],
|
||||||
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
||||||
inheritedAccessModes: [DEFAULT_ACCESS_MODE],
|
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
|
||||||
inheritedFromProvider: false,
|
inheritedFromProvider: false,
|
||||||
overridden: false,
|
overridden: false,
|
||||||
usedBy: [],
|
usedBy: [],
|
||||||
@@ -310,9 +310,9 @@ const buildStorageEntriesFromProvider = (datastoresData) => {
|
|||||||
capacity: ds.capacity || 0,
|
capacity: ds.capacity || 0,
|
||||||
target: '',
|
target: '',
|
||||||
volumeMode: DEFAULT_VOLUME_MODE,
|
volumeMode: DEFAULT_VOLUME_MODE,
|
||||||
accessModes: [DEFAULT_ACCESS_MODE],
|
accessModes: [...DEFAULT_ACCESS_MODES],
|
||||||
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
||||||
inheritedAccessModes: [DEFAULT_ACCESS_MODE],
|
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
|
||||||
inheritedFromProvider: false,
|
inheritedFromProvider: false,
|
||||||
overridden: false,
|
overridden: false,
|
||||||
usedBy: [],
|
usedBy: [],
|
||||||
|
|||||||
@@ -273,6 +273,19 @@ const startMigrationAction = async() => {
|
|||||||
|
|
||||||
await plan.save();
|
await plan.save();
|
||||||
|
|
||||||
|
const planOwnerRef = {
|
||||||
|
apiVersion: FORKLIFT_API_VERSION,
|
||||||
|
kind: 'Plan',
|
||||||
|
name: plan.metadata.name,
|
||||||
|
uid: plan.metadata.uid,
|
||||||
|
blockOwnerDeletion: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
networkMap.metadata.ownerReferences = [planOwnerRef];
|
||||||
|
await networkMap.save();
|
||||||
|
storageMap.metadata.ownerReferences = [planOwnerRef];
|
||||||
|
await storageMap.save();
|
||||||
|
|
||||||
// Kick off the first migration through the model so the Migration payload
|
// Kick off the first migration through the model so the Migration payload
|
||||||
// lives in a single place (also reused by the dashboard start/restart action).
|
// lives in a single place (also reused by the dashboard start/restart action).
|
||||||
await plan.startMigration();
|
await plan.startMigration();
|
||||||
|
|||||||
@@ -8,22 +8,16 @@ import { useI18n } from '@shell/composables/useI18n';
|
|||||||
import { VOLUME_MODE, ACCESS_MODE } from '../../config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '../../config/types';
|
||||||
|
|
||||||
const VOLUME_MODE_OPTIONS = [VOLUME_MODE.FILE_SYSTEM, VOLUME_MODE.BLOCK];
|
const VOLUME_MODE_OPTIONS = [VOLUME_MODE.FILE_SYSTEM, VOLUME_MODE.BLOCK];
|
||||||
const {
|
const ACCESS_MODE_OPTIONS = [ACCESS_MODE.READ_WRITE_ONCE, ACCESS_MODE.READ_WRITE_MANY, ACCESS_MODE.READ_ONLY_MANY];
|
||||||
READ_WRITE_ONCE = 'ReadWriteOnce',
|
|
||||||
READ_ONLY_MANY = 'ReadOnlyMany',
|
|
||||||
READ_WRITE_MANY = 'ReadWriteMany',
|
|
||||||
READ_WRITE_ONCE_POD = 'ReadWriteOncePod'
|
|
||||||
} = ACCESS_MODE || {};
|
|
||||||
const ACCESS_MODE_OPTIONS = [READ_WRITE_ONCE, READ_WRITE_MANY, READ_ONLY_MANY, READ_WRITE_ONCE_POD];
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
storageClassName: { type: String, default: '' },
|
storageClassName: { type: String, default: '' },
|
||||||
providerName: { type: String, default: '' },
|
providerName: { type: String, default: '' },
|
||||||
showInherited: { type: Boolean, default: false },
|
showInherited: { type: Boolean, default: false },
|
||||||
volumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
volumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
||||||
accessModes: { type: Array, default: () => ['ReadWriteMany'] },
|
accessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
|
||||||
inheritedVolumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
inheritedVolumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
||||||
inheritedAccessModes: { type: Array, default: () => ['ReadWriteMany'] },
|
inheritedAccessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['apply', 'close']);
|
const emit = defineEmits(['apply', 'close']);
|
||||||
@@ -32,7 +26,7 @@ const store = useStore();
|
|||||||
const { t } = useI18n(store);
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
const localVolumeMode = ref(props.volumeMode || VOLUME_MODE.FILE_SYSTEM);
|
const localVolumeMode = ref(props.volumeMode || VOLUME_MODE.FILE_SYSTEM);
|
||||||
const localAccessMode = ref(props.accessModes?.[0] || READ_WRITE_MANY);
|
const localAccessMode = ref(props.accessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY);
|
||||||
|
|
||||||
const volumeModeOptions = VOLUME_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
const volumeModeOptions = VOLUME_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
||||||
const accessModeOptions = ACCESS_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
const accessModeOptions = ACCESS_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
||||||
@@ -48,7 +42,7 @@ const apply = () => {
|
|||||||
// Repopulate the dropdowns with the provider default values; the user then applies.
|
// Repopulate the dropdowns with the provider default values; the user then applies.
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
localVolumeMode.value = props.inheritedVolumeMode || VOLUME_MODE.FILE_SYSTEM;
|
localVolumeMode.value = props.inheritedVolumeMode || VOLUME_MODE.FILE_SYSTEM;
|
||||||
localAccessMode.value = props.inheritedAccessModes?.[0] || READ_WRITE_MANY;
|
localAccessMode.value = props.inheritedAccessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
|
|||||||
@@ -111,17 +111,15 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
||||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
preview: true,
|
experimental: true,
|
||||||
featureFlag: 'longhornV2LVMSupport'
|
featureFlag: 'longhornV2LVMSupport'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
preview: true,
|
|
||||||
featureFlag: 'longhornV2HugepageSettings'
|
featureFlag: 'longhornV2HugepageSettings'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
|
||||||
kind: 'number',
|
kind: 'number',
|
||||||
preview: true,
|
|
||||||
featureFlag: 'longhornV2HugepageSettings'
|
featureFlag: 'longhornV2HugepageSettings'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ export const VOLUME_MODE = {
|
|||||||
FILE_SYSTEM: 'Filesystem'
|
FILE_SYSTEM: 'Filesystem'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ACCESS_MODE = {
|
||||||
|
READ_WRITE_MANY: 'ReadWriteMany',
|
||||||
|
READ_WRITE_ONCE: 'ReadWriteOnce',
|
||||||
|
READ_ONLY_MANY: 'ReadOnlyMany',
|
||||||
|
READ_WRITE_ONCE_POD: 'ReadWriteOncePod',
|
||||||
|
};
|
||||||
|
|
||||||
export const NETWORK_PROTOCOL = {
|
export const NETWORK_PROTOCOL = {
|
||||||
IPv4: 'IPv4',
|
IPv4: 'IPv4',
|
||||||
IPv6: 'IPv6',
|
IPv6: 'IPv6',
|
||||||
|
|||||||
@@ -390,17 +390,14 @@ export default {
|
|||||||
:vmi-resource="vmi"
|
:vmi-resource="vmi"
|
||||||
:vmim-resource="vmim"
|
:vmim-resource="vmim"
|
||||||
/>
|
/>
|
||||||
<div
|
<DashboardMetrics
|
||||||
v-if="showVmMetrics && liveMigrationProgressEnabled"
|
v-if="showVmMetrics && liveMigrationProgressEnabled"
|
||||||
class="migration-metrics-scroll mb-30"
|
:detail-url="VM_MIGRATION_DETAIL_URL"
|
||||||
>
|
graph-height="640px"
|
||||||
<DashboardMetrics
|
:has-summary-and-detail="false"
|
||||||
:detail-url="VM_MIGRATION_DETAIL_URL"
|
:vars="graphVars"
|
||||||
graph-height="960px"
|
class="mb-30"
|
||||||
:has-summary-and-detail="false"
|
/>
|
||||||
:vars="graphVars"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Events
|
<Events
|
||||||
v-if="liveMigrationProgressEnabled"
|
v-if="liveMigrationProgressEnabled"
|
||||||
:resource="vmi"
|
:resource="vmi"
|
||||||
@@ -439,10 +436,3 @@ export default {
|
|||||||
</Tabbed>
|
</Tabbed>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.migration-metrics-scroll {
|
|
||||||
max-height: 640px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
@@ -10,6 +10,10 @@ import { HCI_SETTING } from '../../config/settings';
|
|||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
|
|
||||||
|
const {
|
||||||
|
READ_WRITE_ONCE, READ_ONLY_MANY, READ_WRITE_MANY, READ_WRITE_ONCE_POD
|
||||||
|
} = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CDISettings',
|
name: 'CDISettings',
|
||||||
|
|
||||||
@@ -90,10 +94,10 @@ export default {
|
|||||||
|
|
||||||
accessModeOptions() {
|
accessModeOptions() {
|
||||||
return [
|
return [
|
||||||
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
|
{ label: READ_WRITE_ONCE, value: READ_WRITE_ONCE },
|
||||||
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
|
{ label: READ_ONLY_MANY, value: READ_ONLY_MANY },
|
||||||
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
|
{ label: READ_WRITE_MANY, value: READ_WRITE_MANY },
|
||||||
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
|
{ label: READ_WRITE_ONCE_POD, value: READ_WRITE_ONCE_POD },
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
@@ -177,8 +178,17 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async saveVMT(buttonCb) {
|
async saveVMT(buttonCb) {
|
||||||
|
this.errors = [];
|
||||||
this.parseVM();
|
this.parseVM();
|
||||||
|
|
||||||
|
this.validateCloudInit();
|
||||||
|
|
||||||
|
if (this.errors.length) {
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const templates = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_TEMPLATE });
|
const templates = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_TEMPLATE });
|
||||||
const template = templates.find( (O) => O.metadata.name === this.templateValue.metadata.name);
|
const template = templates.find( (O) => O.metadata.name === this.templateValue.metadata.name);
|
||||||
|
|
||||||
@@ -237,6 +247,24 @@ export default {
|
|||||||
this.$refs.yamlEditor?.refresh();
|
this.$refs.yamlEditor?.refresh();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
validateCloudInit() {
|
||||||
|
if (this.userScript) {
|
||||||
|
try {
|
||||||
|
jsyaml.load(this.userScript);
|
||||||
|
} catch (e) {
|
||||||
|
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.user.invalidYaml'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.networkScript) {
|
||||||
|
try {
|
||||||
|
jsyaml.load(this.networkScript);
|
||||||
|
} catch (e) {
|
||||||
|
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.network.invalidYaml'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ import { HCI, VOLUME_SNAPSHOT } from '../types';
|
|||||||
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||||
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
||||||
import { GIBIBYTE } from '../utils/unit';
|
import { GIBIBYTE } from '../utils/unit';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
import { isInternalStorageClass } from '../utils/storage-class';
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
|
const { READ_WRITE_MANY, READ_WRITE_ONCE, READ_ONLY_MANY } = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterVolume',
|
name: 'HarvesterVolume',
|
||||||
|
|
||||||
@@ -83,7 +85,7 @@ export default {
|
|||||||
if (this.mode === _CREATE) {
|
if (this.mode === _CREATE) {
|
||||||
// default volumeMode to Block
|
// default volumeMode to Block
|
||||||
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
|
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
|
||||||
this.value.spec.accessModes = ['ReadWriteMany'];
|
this.value.spec.accessModes = [READ_WRITE_MANY];
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = this.value?.spec?.resources?.requests?.storage || null;
|
const storage = this.value?.spec?.resources?.requests?.storage || null;
|
||||||
@@ -160,7 +162,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
accessModeOptions() {
|
accessModeOptions() {
|
||||||
return ['ReadWriteOnce', 'ReadWriteMany', 'ReadOnlyMany'];
|
return [READ_WRITE_ONCE, READ_WRITE_MANY, READ_ONLY_MANY];
|
||||||
},
|
},
|
||||||
|
|
||||||
imageOption() {
|
imageOption() {
|
||||||
@@ -355,7 +357,7 @@ export default {
|
|||||||
|
|
||||||
getAccessMode() {
|
getAccessMode() {
|
||||||
if (!this.longhornV2LVMSupport) {
|
if (!this.longhornV2LVMSupport) {
|
||||||
return ['ReadWriteMany'];
|
return [READ_WRITE_MANY];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
|
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
|
||||||
@@ -371,7 +373,7 @@ export default {
|
|||||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.value.thirdPartyStorageFeatureEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.value.thirdPartyStorageFeatureEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
return readWriteOnce ? [READ_WRITE_ONCE] : [READ_WRITE_MANY];
|
||||||
},
|
},
|
||||||
buildDataVolumeObj() {
|
buildDataVolumeObj() {
|
||||||
const storage = {
|
const storage = {
|
||||||
|
|||||||
@@ -246,27 +246,4 @@ export default {
|
|||||||
.group-actions {
|
.group-actions {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match the enable/disable passthrough bulk-action buttons height to the .btn (40px) on the right.
|
|
||||||
:deep(.bulk .rc-button.btn-medium.bulk-action:not(.btn-sm)) {
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the collapsed "Actions" dropdown button 40px too.
|
|
||||||
:deep(.bulk .rc-button.btn-medium.bulk-actions-dropdown:not(.btn-sm)) {
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lay the bulk row out with flex so the "N selected" label can wrap below the buttons.
|
|
||||||
:deep(.bulk) {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.bulk .action-availability) {
|
|
||||||
flex-basis: 100%;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|||||||
import Banner from '@components/Banner/Banner.vue';
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
import remove from 'lodash/remove';
|
import remove from 'lodash/remove';
|
||||||
import { set } from '@shell/utils/object';
|
import { set } from '@shell/utils/object';
|
||||||
import { uniq } from '@shell/utils/array';
|
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import DeviceList from './DeviceList';
|
import DeviceList from './DeviceList';
|
||||||
import CompatibilityMatrix from '../CompatibilityMatrix';
|
import CompatibilityMatrix from '../CompatibilityMatrix';
|
||||||
@@ -61,15 +60,7 @@ export default {
|
|||||||
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
row.allowDisable = !vmDeviceNames.includes(row.metadata.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
// When off, read from spec; otherwise the spec name is a placeholder ('provisioned'),
|
vmDevices.forEach(({ name }) => {
|
||||||
// so read the real allocated device names from the deviceAllocationDetails annotation.
|
|
||||||
const hostDeviceNames = this.vm.isOff ? [
|
|
||||||
...vmDevices.map(({ name }) => name),
|
|
||||||
] : [
|
|
||||||
...Object.values(this.vm?.provisionedHostDevices || {}).reduce((acc, devices) => [...acc, ...devices], []),
|
|
||||||
];
|
|
||||||
|
|
||||||
uniq(hostDeviceNames).forEach((name) => {
|
|
||||||
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
|
if (this.enabledDevices.find((device) => device?.metadata?.name === name)) {
|
||||||
selectedDevices.push(name);
|
selectedDevices.push(name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ import { PLUGIN_DEVELOPER, DEV } from '@shell/store/prefs';
|
|||||||
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
|
import { VOLUME_HOTPLUG_ACTION, SOURCE_TYPE } from '../../../config/harvester-map';
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||||
import { EMPTY_IMAGE } from '../../../utils/vm';
|
import { EMPTY_IMAGE } from '../../../utils/vm';
|
||||||
|
|
||||||
|
const { READ_WRITE_MANY } = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ export default {
|
|||||||
|
|
||||||
customAccessMode: {
|
customAccessMode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'ReadWriteMany'
|
default: READ_WRITE_MANY
|
||||||
},
|
},
|
||||||
|
|
||||||
resourceType: {
|
resourceType: {
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storagec
|
|||||||
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
||||||
import { GIBIBYTE } from '../../../../utils/unit';
|
import { GIBIBYTE } from '../../../../utils/unit';
|
||||||
import { isInternalStorageClass } from '../../../../utils/storage-class';
|
import { isInternalStorageClass } from '../../../../utils/storage-class';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
|
const { READ_WRITE_MANY, READ_WRITE_ONCE } = ACCESS_MODE;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditVolume',
|
name: 'HarvesterEditVolume',
|
||||||
@@ -205,7 +207,7 @@ export default {
|
|||||||
|
|
||||||
getAccessMode(storageClassName) {
|
getAccessMode(storageClassName) {
|
||||||
if (!this.longhornV2LVMSupport) {
|
if (!this.longhornV2LVMSupport) {
|
||||||
return 'ReadWriteMany';
|
return READ_WRITE_MANY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||||
@@ -216,7 +218,7 @@ export default {
|
|||||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.thirdPartyStorageClassEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.thirdPartyStorageClassEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return readWriteOnce ? 'ReadWriteOnce' : 'ReadWriteMany';
|
return readWriteOnce ? READ_WRITE_ONCE : READ_WRITE_MANY;
|
||||||
},
|
},
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
@@ -379,6 +380,7 @@ export default {
|
|||||||
|
|
||||||
this.validateCPUMemory();
|
this.validateCPUMemory();
|
||||||
this.validateWindowsSysprep();
|
this.validateWindowsSysprep();
|
||||||
|
this.validateCloudInit();
|
||||||
|
|
||||||
// block create VM flow if has validation errors
|
// block create VM flow if has validation errors
|
||||||
if (this.errors.length) {
|
if (this.errors.length) {
|
||||||
@@ -424,6 +426,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
validateCloudInit() {
|
||||||
|
if (this.userScript) {
|
||||||
|
try {
|
||||||
|
jsyaml.load(this.userScript);
|
||||||
|
} catch (e) {
|
||||||
|
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.user.invalidYaml'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.networkScript) {
|
||||||
|
try {
|
||||||
|
jsyaml.load(this.networkScript);
|
||||||
|
} catch (e) {
|
||||||
|
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.network.invalidYaml'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async saveSingle(buttonCb) {
|
async saveSingle(buttonCb) {
|
||||||
this.parseVM();
|
this.parseVM();
|
||||||
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
||||||
|
|||||||
@@ -830,18 +830,20 @@ harvester:
|
|||||||
label: User Data Template
|
label: User Data Template
|
||||||
title: "User Data:"
|
title: "User Data:"
|
||||||
tip: "You can specify user data to configure an instance or run a configuration script during launch. If you launch more than one instance at a time, the user data is available to all the instances in that reservation. <a href='https://cloudinit.readthedocs.io/en/latest/topics/examples.html' target='_blank'>Learn more</a>"
|
tip: "You can specify user data to configure an instance or run a configuration script during launch. If you launch more than one instance at a time, the user data is available to all the instances in that reservation. <a href='https://cloudinit.readthedocs.io/en/latest/topics/examples.html' target='_blank'>Learn more</a>"
|
||||||
|
invalidYaml: 'Invalid User Data YAML syntax'
|
||||||
network:
|
network:
|
||||||
label: Network Data Template
|
label: Network Data Template
|
||||||
title: "Network Data:"
|
title: "Network Data:"
|
||||||
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
||||||
|
invalidYaml: 'Invalid Network Data YAML syntax'
|
||||||
sysprep:
|
sysprep:
|
||||||
title: Windows Unattended & Sysprep Configuration
|
title: Windows Sysprep Configuration
|
||||||
description: "Configure Windows automated installation using autounattend.xml. The configuration will be stored in a Kubernetes Secret and mounted as a CD-ROM during installation. <a href='https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/automate-windows-setup' target='_blank'>Learn more</a>"
|
description: "Configure Windows automated installation using autounattend.xml. The configuration will be stored in a Kubernetes Secret and mounted as a CD-ROM during installation. <a href='https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/automate-windows-setup' target='_blank'>Learn more</a>"
|
||||||
createNew: Create new...
|
createNew: Create new...
|
||||||
createNewTitle: Create Windows Unattended & Sysprep Template
|
createNewTitle: Create Windows Sysprep Template
|
||||||
xmlContent: autounattend.xml content
|
xmlContent: autounattend.xml content
|
||||||
secret:
|
secret:
|
||||||
label: Windows Unattended & Sysprep Template
|
label: Windows Sysprep Template
|
||||||
validation:
|
validation:
|
||||||
invalidXml: Invalid XML syntax
|
invalidXml: Invalid XML syntax
|
||||||
invalidNamespace: Invalid unattend.xml namespace. Expected 'urn:schemas-microsoft-com:unattend'
|
invalidNamespace: Invalid unattend.xml namespace. Expected 'urn:schemas-microsoft-com:unattend'
|
||||||
@@ -2417,15 +2419,7 @@ typeDescription:
|
|||||||
harvester: "Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos and color scheme."
|
harvester: "Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos and color scheme."
|
||||||
|
|
||||||
advancedSettings:
|
advancedSettings:
|
||||||
tags:
|
experimental: 'Experimental features allow users to test and evaluate early-access functionality prior to official supported releases'
|
||||||
modified:
|
|
||||||
label: Modified
|
|
||||||
experimental:
|
|
||||||
label: Experimental
|
|
||||||
description: 'Experimental: The feature is incomplete but its essential functionality is operational. The feature is not recommended for use in production environments.'
|
|
||||||
preview:
|
|
||||||
label: Technical Preview
|
|
||||||
description: 'Technical Preview: The feature is almost complete and its functionality is not expected to change significantly. Explore the feature extensively before using in production environments.'
|
|
||||||
descriptions:
|
descriptions:
|
||||||
'harv-vlan': Default Network Interface name of the VLAN network.
|
'harv-vlan': Default Network Interface name of the VLAN network.
|
||||||
'harv-backup-target': Custom backup target to store virtual machine backups.
|
'harv-backup-target': Custom backup target to store virtual machine backups.
|
||||||
|
|||||||
@@ -38,10 +38,6 @@ export const SSH_EXISTING_TYPE = {
|
|||||||
export default {
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
hasCloudConfigComment(userScript) {
|
hasCloudConfigComment(userScript) {
|
||||||
if (typeof userScript === 'string' && /^\s*#cloud-config(\s|$)/.test(userScript)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that userData contains: #cloud-config
|
// Check that userData contains: #cloud-config
|
||||||
const userDataDoc = userScript ? YAML.parseDocument(userScript) : YAML.parseDocument({});
|
const userDataDoc = userScript ? YAML.parseDocument(userScript) : YAML.parseDocument({});
|
||||||
const items = userDataDoc?.contents?.items || [];
|
const items = userDataDoc?.contents?.items || [];
|
||||||
@@ -56,14 +52,6 @@ export default {
|
|||||||
exist = true;
|
exist = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userDataDoc?.contents?.commentBefore === 'cloud-config' || userDataDoc?.contents?.commentBefore?.includes('cloud-config\n')) {
|
|
||||||
exist = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userDataDoc?.contents?.comment === 'cloud-config' || userDataDoc?.contents?.comment?.includes('cloud-config\n')) {
|
|
||||||
exist = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
const key = item.key;
|
const key = item.key;
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import { HCI } from '../../types';
|
|||||||
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
import { parseVolumeClaimTemplates, EMPTY_IMAGE } from '../../utils/vm';
|
||||||
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
import impl, { QGA_JSON, USB_TABLET } from './impl';
|
||||||
import { GIBIBYTE } from '../../utils/unit';
|
import { GIBIBYTE } from '../../utils/unit';
|
||||||
import { VOLUME_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE, ACCESS_MODE, FILESYSTEM_SOURCE_TYPE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
|
const { READ_WRITE_MANY } = ACCESS_MODE;
|
||||||
|
|
||||||
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||||
|
|
||||||
@@ -276,7 +278,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
customAccessMode() {
|
customAccessMode() {
|
||||||
return this.storageClassSetting.accessModes || 'ReadWriteMany';
|
return this.storageClassSetting.accessModes || READ_WRITE_MANY;
|
||||||
},
|
},
|
||||||
|
|
||||||
isWindows() {
|
isWindows() {
|
||||||
@@ -504,7 +506,7 @@ export default {
|
|||||||
id: randomStr(5),
|
id: randomStr(5),
|
||||||
source: SOURCE_TYPE.IMAGE,
|
source: SOURCE_TYPE.IMAGE,
|
||||||
name: 'disk-0',
|
name: 'disk-0',
|
||||||
accessMode: 'ReadWriteMany', // root disk only support LHv1 volume, should be RWX
|
accessMode: READ_WRITE_MANY, // root disk only support LHv1 volume, should be RWX
|
||||||
bus,
|
bus,
|
||||||
volumeName: '',
|
volumeName: '',
|
||||||
size,
|
size,
|
||||||
@@ -570,7 +572,7 @@ export default {
|
|||||||
const pvcResource = allPVCs.find( (O) => O.id === `${ namespace }/${ volume?.persistentVolumeClaim?.claimName }`);
|
const pvcResource = allPVCs.find( (O) => O.id === `${ namespace }/${ volume?.persistentVolumeClaim?.claimName }`);
|
||||||
|
|
||||||
source = SOURCE_TYPE.ATTACH_VOLUME;
|
source = SOURCE_TYPE.ATTACH_VOLUME;
|
||||||
accessMode = pvcResource?.spec?.accessModes?.[0] || 'ReadWriteMany';
|
accessMode = pvcResource?.spec?.accessModes?.[0] || READ_WRITE_MANY;
|
||||||
size = pvcResource?.spec?.resources?.requests?.storage || '10Gi';
|
size = pvcResource?.spec?.resources?.requests?.storage || '10Gi';
|
||||||
storageClassName = pvcResource?.spec?.storageClassName;
|
storageClassName = pvcResource?.spec?.storageClassName;
|
||||||
volumeMode = pvcResource?.spec?.volumeMode || VOLUME_MODE.BLOCK;
|
volumeMode = pvcResource?.spec?.volumeMode || VOLUME_MODE.BLOCK;
|
||||||
@@ -1209,10 +1211,9 @@ export default {
|
|||||||
|
|
||||||
userDataDoc = config.installAgent ? this.mergeQGA({ userDataDoc, ...config }) : this.deleteQGA({ userDataDoc, ...config });
|
userDataDoc = config.installAgent ? this.mergeQGA({ userDataDoc, ...config }) : this.deleteQGA({ userDataDoc, ...config });
|
||||||
const userDataYaml = userDataDoc.toString();
|
const userDataYaml = userDataDoc.toString();
|
||||||
const userDataValue = userDataDoc.toJSON();
|
|
||||||
|
|
||||||
if (userDataValue === null || (typeof userDataValue === 'object' && !Array.isArray(userDataValue) && isEmpty(userDataValue))) {
|
if (userDataYaml === '{}\n') {
|
||||||
// Empty userData should not create cloud-init content like `null`.
|
// When the YAML parsed value is '{}\n', it means that the userData is empty, then undefined is returned.
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import SYSTEM_NAMESPACES from '@shell/config/system-namespaces';
|
|||||||
import { get } from '@shell/utils/object';
|
import { get } from '@shell/utils/object';
|
||||||
import { NAMESPACE } from '@shell/config/types';
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
|
||||||
import { FORKLIFT_NAMESPACE } from '@pkg/harvester/config/harvester-map';
|
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
|
|
||||||
const OBSCURE_NAMESPACE_PREFIX = [
|
const OBSCURE_NAMESPACE_PREFIX = [
|
||||||
@@ -94,10 +93,6 @@ export default class HciNamespace extends namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isSystem() {
|
get isSystem() {
|
||||||
if (this.metadata?.name === FORKLIFT_NAMESPACE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemNamespaces = ['fleet-default'];
|
const systemNamespaces = ['fleet-default'];
|
||||||
|
|
||||||
if (systemNamespaces.includes(this.metadata.name)) {
|
if (systemNamespaces.includes(this.metadata.name)) {
|
||||||
|
|||||||
@@ -1315,18 +1315,6 @@ export default class VirtVm extends HarvesterResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get provisionedHostDevices() {
|
|
||||||
// Rancher-provisioned VMs store hostDevices.name as a placeholder ('provisioned'); the
|
|
||||||
// real allocated PCI device names live in this annotation, keyed by resourceName.
|
|
||||||
try {
|
|
||||||
const deviceAllocationDetails = JSON.parse(this.metadata?.annotations[HCI_ANNOTATIONS.VM_DEVICE_ALLOCATION_DETAILS] || '{}');
|
|
||||||
|
|
||||||
return deviceAllocationDetails?.hostdevices || {};
|
|
||||||
} catch (error) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get schedulingVMBackupFeatureEnabled() {
|
get schedulingVMBackupFeatureEnabled() {
|
||||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
|
return this.$rootGetters['harvester-common/getFeatureEnabled']('schedulingVMBackup');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.9.0-rc7",
|
"version": "1.10.0-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
|||||||
@@ -68,35 +68,29 @@ export default defineComponent({
|
|||||||
return (this.value || []).map((plan) => {
|
return (this.value || []).map((plan) => {
|
||||||
const planName = plan?.metadata?.name || '-';
|
const planName = plan?.metadata?.name || '-';
|
||||||
const namespace = plan?.metadata?.namespace || '';
|
const namespace = plan?.metadata?.namespace || '';
|
||||||
const networkMapRef = this.getPlanMapRef(plan, 'network');
|
|
||||||
const storageMapRef = this.getPlanMapRef(plan, 'storage');
|
|
||||||
|
|
||||||
const migrations = existing[HCI.FORKLIFT_MIGRATION]
|
const migrations = existing[HCI.FORKLIFT_MIGRATION]
|
||||||
.filter((resource) => this.ownedByPlan(resource, plan))
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
.map((resource) => resource?.metadata?.name)
|
.map((resource) => ({
|
||||||
.filter(Boolean);
|
name: resource?.metadata?.name,
|
||||||
|
namespace: resource?.metadata?.namespace || namespace,
|
||||||
|
}));
|
||||||
|
|
||||||
const matchedNetworkMap = networkMapRef ? existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => resource?.metadata?.name === networkMapRef.name && resource?.metadata?.namespace === networkMapRef.namespace) : null;
|
const matchedNetworkMap = existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => this.ownedByPlan(resource, plan));
|
||||||
const matchedStorageMap = storageMapRef ? existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => resource?.metadata?.name === storageMapRef.name && resource?.metadata?.namespace === storageMapRef.namespace) : null;
|
const matchedStorageMap = existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => this.ownedByPlan(resource, plan));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
planName,
|
planName,
|
||||||
namespace,
|
namespace,
|
||||||
migrations,
|
migrations,
|
||||||
networkMap: matchedNetworkMap ? {
|
networkMap: matchedNetworkMap ? {
|
||||||
namespace: matchedNetworkMap?.metadata?.namespace || networkMapRef?.namespace || namespace,
|
namespace: matchedNetworkMap?.metadata?.namespace || namespace,
|
||||||
name: matchedNetworkMap?.metadata?.name,
|
name: matchedNetworkMap?.metadata?.name,
|
||||||
} : (networkMapRef ? {
|
} : null,
|
||||||
namespace: networkMapRef.namespace,
|
|
||||||
name: networkMapRef.name,
|
|
||||||
} : null),
|
|
||||||
storageMap: matchedStorageMap ? {
|
storageMap: matchedStorageMap ? {
|
||||||
namespace: matchedStorageMap?.metadata?.namespace || storageMapRef?.namespace || namespace,
|
namespace: matchedStorageMap?.metadata?.namespace || namespace,
|
||||||
name: matchedStorageMap?.metadata?.name,
|
name: matchedStorageMap?.metadata?.name,
|
||||||
} : (storageMapRef ? {
|
} : null,
|
||||||
namespace: storageMapRef.namespace,
|
|
||||||
name: storageMapRef.name,
|
|
||||||
} : null),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -119,20 +113,6 @@ export default defineComponent({
|
|||||||
methods: {
|
methods: {
|
||||||
resourceNames,
|
resourceNames,
|
||||||
|
|
||||||
getPlanMapRef(plan, mapType) {
|
|
||||||
const ref = plan?.spec?.map?.[mapType];
|
|
||||||
const name = ref?.name;
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
namespace: ref?.namespace || plan?.metadata?.namespace || '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether a related resource (migration / network map / storage map)
|
* Determine whether a related resource (migration / network map / storage map)
|
||||||
* is owned by the given plan by inspecting its `metadata.ownerReferences`.
|
* is owned by the given plan by inspecting its `metadata.ownerReferences`.
|
||||||
@@ -157,8 +137,9 @@ export default defineComponent({
|
|||||||
const existing = this.existingRelatedResources;
|
const existing = this.existingRelatedResources;
|
||||||
const targets = new Map();
|
const targets = new Map();
|
||||||
|
|
||||||
const addTarget = (type, name, namespace) => {
|
const addTarget = (type, resource) => {
|
||||||
const ns = namespace || '';
|
const name = resource?.metadata?.name;
|
||||||
|
const ns = resource?.metadata?.namespace;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return;
|
return;
|
||||||
@@ -172,20 +153,17 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const plan of this.value || []) {
|
for (const plan of this.value || []) {
|
||||||
const networkMapRef = this.getPlanMapRef(plan, 'network');
|
existing[HCI.FORKLIFT_NETWORK_MAP]
|
||||||
const storageMapRef = this.getPlanMapRef(plan, 'storage');
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
|
.forEach((resource) => addTarget(HCI.FORKLIFT_NETWORK_MAP, resource));
|
||||||
|
|
||||||
if (networkMapRef) {
|
existing[HCI.FORKLIFT_STORAGE_MAP]
|
||||||
addTarget(HCI.FORKLIFT_NETWORK_MAP, networkMapRef.name, networkMapRef.namespace);
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
}
|
.forEach((resource) => addTarget(HCI.FORKLIFT_STORAGE_MAP, resource));
|
||||||
|
|
||||||
if (storageMapRef) {
|
|
||||||
addTarget(HCI.FORKLIFT_STORAGE_MAP, storageMapRef.name, storageMapRef.namespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
existing[HCI.FORKLIFT_MIGRATION]
|
existing[HCI.FORKLIFT_MIGRATION]
|
||||||
.filter((resource) => this.ownedByPlan(resource, plan))
|
.filter((resource) => this.ownedByPlan(resource, plan))
|
||||||
.forEach((resource) => addTarget(HCI.FORKLIFT_MIGRATION, resource?.metadata?.name, resource?.metadata?.namespace));
|
.forEach((resource) => addTarget(HCI.FORKLIFT_MIGRATION, resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...targets.values()];
|
return [...targets.values()];
|
||||||
@@ -278,11 +256,11 @@ export default defineComponent({
|
|||||||
<div>• {{ t('harvester.addons.vmMigration.labels.migration') }}:</div>
|
<div>• {{ t('harvester.addons.vmMigration.labels.migration') }}:</div>
|
||||||
<template v-if="summary.migrations.length">
|
<template v-if="summary.migrations.length">
|
||||||
<div
|
<div
|
||||||
v-for="migrationName in summary.migrations"
|
v-for="migration in summary.migrations"
|
||||||
:key="migrationName"
|
:key="`${ migration.namespace }/${ migration.name }`"
|
||||||
>
|
>
|
||||||
<div class="ml-20">
|
<div class="ml-20">
|
||||||
- {{ migrationName }}
|
- {{ migration.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2825,10 +2825,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||||
|
|
||||||
"@types/lodash@4.17.25":
|
"@types/lodash@4.17.24":
|
||||||
version "4.17.25"
|
version "4.17.24"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.25.tgz#69765ac7bcddb0eb072961cf292524a8f5b3c2c0"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.24.tgz#4ae334fc62c0e915ca8ed8e35dcc6d4eeb29215f"
|
||||||
integrity sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==
|
integrity sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==
|
||||||
|
|
||||||
"@types/mime@^1":
|
"@types/mime@^1":
|
||||||
version "1.3.5"
|
version "1.3.5"
|
||||||
@@ -4367,6 +4367,24 @@ bn.js@^5.2.1, bn.js@^5.2.2:
|
|||||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.3.tgz#16a9e409616b23fef3ccbedb8d42f13bff80295e"
|
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.3.tgz#16a9e409616b23fef3ccbedb8d42f13bff80295e"
|
||||||
integrity sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==
|
integrity sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==
|
||||||
|
|
||||||
|
body-parser@~1.20.3:
|
||||||
|
version "1.20.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f"
|
||||||
|
integrity sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==
|
||||||
|
dependencies:
|
||||||
|
bytes "~3.1.2"
|
||||||
|
content-type "~1.0.5"
|
||||||
|
debug "2.6.9"
|
||||||
|
depd "2.0.0"
|
||||||
|
destroy "~1.2.0"
|
||||||
|
http-errors "~2.0.1"
|
||||||
|
iconv-lite "~0.4.24"
|
||||||
|
on-finished "~2.4.1"
|
||||||
|
qs "~6.14.0"
|
||||||
|
raw-body "~2.5.3"
|
||||||
|
type-is "~1.6.18"
|
||||||
|
unpipe "~1.0.0"
|
||||||
|
|
||||||
body-parser@~1.20.5:
|
body-parser@~1.20.5:
|
||||||
version "1.20.6"
|
version "1.20.6"
|
||||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.6.tgz#60c789c78e0992d906da0a29d71ae01d15c1ed76"
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.6.tgz#60c789c78e0992d906da0a29d71ae01d15c1ed76"
|
||||||
@@ -6667,7 +6685,7 @@ expect@^27.5.1:
|
|||||||
jest-matcher-utils "^27.5.1"
|
jest-matcher-utils "^27.5.1"
|
||||||
jest-message-util "^27.5.1"
|
jest-message-util "^27.5.1"
|
||||||
|
|
||||||
express@4.22.2, express@^4.17.3:
|
express@4.22.2:
|
||||||
version "4.22.2"
|
version "4.22.2"
|
||||||
resolved "https://registry.yarnpkg.com/express/-/express-4.22.2.tgz#c17ae0981e5efc24b22272f0e041c4662503b700"
|
resolved "https://registry.yarnpkg.com/express/-/express-4.22.2.tgz#c17ae0981e5efc24b22272f0e041c4662503b700"
|
||||||
integrity sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==
|
integrity sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==
|
||||||
@@ -6704,6 +6722,43 @@ express@4.22.2, express@^4.17.3:
|
|||||||
utils-merge "1.0.1"
|
utils-merge "1.0.1"
|
||||||
vary "~1.1.2"
|
vary "~1.1.2"
|
||||||
|
|
||||||
|
express@^4.17.3:
|
||||||
|
version "4.22.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/express/-/express-4.22.1.tgz#1de23a09745a4fffdb39247b344bb5eaff382069"
|
||||||
|
integrity sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==
|
||||||
|
dependencies:
|
||||||
|
accepts "~1.3.8"
|
||||||
|
array-flatten "1.1.1"
|
||||||
|
body-parser "~1.20.3"
|
||||||
|
content-disposition "~0.5.4"
|
||||||
|
content-type "~1.0.4"
|
||||||
|
cookie "~0.7.1"
|
||||||
|
cookie-signature "~1.0.6"
|
||||||
|
debug "2.6.9"
|
||||||
|
depd "2.0.0"
|
||||||
|
encodeurl "~2.0.0"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
etag "~1.8.1"
|
||||||
|
finalhandler "~1.3.1"
|
||||||
|
fresh "~0.5.2"
|
||||||
|
http-errors "~2.0.0"
|
||||||
|
merge-descriptors "1.0.3"
|
||||||
|
methods "~1.1.2"
|
||||||
|
on-finished "~2.4.1"
|
||||||
|
parseurl "~1.3.3"
|
||||||
|
path-to-regexp "~0.1.12"
|
||||||
|
proxy-addr "~2.0.7"
|
||||||
|
qs "~6.14.0"
|
||||||
|
range-parser "~1.2.1"
|
||||||
|
safe-buffer "5.2.1"
|
||||||
|
send "~0.19.0"
|
||||||
|
serve-static "~1.16.2"
|
||||||
|
setprototypeof "1.2.0"
|
||||||
|
statuses "~2.0.1"
|
||||||
|
type-is "~1.6.18"
|
||||||
|
utils-merge "1.0.1"
|
||||||
|
vary "~1.1.2"
|
||||||
|
|
||||||
extend-shallow@^2.0.1:
|
extend-shallow@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
|
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
|
||||||
@@ -10329,7 +10384,7 @@ punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.0, punycode@^2.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||||
|
|
||||||
qs@6.15.3, qs@^6.12.3, qs@~6.15.1:
|
qs@6.15.3, qs@^6.12.3, qs@~6.14.0, qs@~6.15.1:
|
||||||
version "6.15.3"
|
version "6.15.3"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.3.tgz#76852132a58ed5c7c0ef67e4441b9bb5d6061b3b"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.3.tgz#76852132a58ed5c7c0ef67e4441b9bb5d6061b3b"
|
||||||
integrity sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==
|
integrity sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==
|
||||||
|
|||||||
Reference in New Issue
Block a user