mirror of https://github.com/rancher/dashboard.git
1343 lines
40 KiB
Vue
1343 lines
40 KiB
Vue
<script>
|
|
import jsyaml from 'js-yaml';
|
|
import merge from 'lodash/merge';
|
|
import isEqual from 'lodash/isEqual';
|
|
import { mapPref, DIFF } from '@/store/prefs';
|
|
|
|
import Banner from '@/components/Banner';
|
|
import ButtonGroup from '@/components/ButtonGroup';
|
|
import ChartReadme from '@/components/ChartReadme';
|
|
import Checkbox from '@/components/form/Checkbox';
|
|
import LabeledSelect from '@/components/form/LabeledSelect';
|
|
import LazyImage from '@/components/LazyImage';
|
|
import Loading from '@/components/Loading';
|
|
import NameNsDescription from '@/components/form/NameNsDescription';
|
|
import ResourceCancelModal from '@/components/ResourceCancelModal';
|
|
import Questions from '@/components/Questions';
|
|
import Tabbed from '@/components/Tabbed';
|
|
import UnitInput from '@/components/form/UnitInput';
|
|
import YamlEditor, { EDITOR_MODES } from '@/components/YamlEditor';
|
|
import Wizard from '@/components/Wizard';
|
|
import ChartMixin from '@/mixins/chart';
|
|
import ChildHook, { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@/mixins/child-hook';
|
|
import { CATALOG, MANAGEMENT } from '@/config/types';
|
|
import {
|
|
CHART, FROM_CLUSTER, FROM_TOOLS, NAMESPACE, REPO, REPO_TYPE, VERSION, _FLAGGED
|
|
} from '@/config/query-params';
|
|
import { CATALOG as CATALOG_ANNOTATIONS, DESCRIPTION as DESCRIPTION_ANNOTATION, PROJECT } from '@/config/labels-annotations';
|
|
|
|
import { exceptionToErrorsArray } from '@/utils/error';
|
|
import { clone, diff, get, set } from '@/utils/object';
|
|
import { findBy, insertAt } from '@/utils/array';
|
|
import Vue from 'vue';
|
|
import { saferDump } from '@/utils/create-yaml';
|
|
import { DEFAULT_WORKSPACE } from '@/models/provisioning.cattle.io.cluster';
|
|
|
|
const VALUES_STATE = {
|
|
FORM: 'FORM',
|
|
YAML: 'YAML',
|
|
DIFF: 'DIFF'
|
|
};
|
|
|
|
export default {
|
|
name: 'Install',
|
|
|
|
components: {
|
|
Banner,
|
|
ButtonGroup,
|
|
ChartReadme,
|
|
Checkbox,
|
|
LabeledSelect,
|
|
LazyImage,
|
|
Loading,
|
|
NameNsDescription,
|
|
ResourceCancelModal,
|
|
Questions,
|
|
Tabbed,
|
|
UnitInput,
|
|
YamlEditor,
|
|
Wizard
|
|
},
|
|
|
|
mixins: [
|
|
ChildHook,
|
|
ChartMixin
|
|
],
|
|
|
|
async fetch() {
|
|
await this.fetchChart();
|
|
|
|
this.errors = [];
|
|
|
|
this.defaultRegistrySetting = await this.$store.dispatch('management/find', {
|
|
type: MANAGEMENT.SETTING,
|
|
id: 'system-default-registry'
|
|
});
|
|
|
|
this.serverUrlSetting = await this.$store.dispatch('management/find', {
|
|
type: MANAGEMENT.SETTING,
|
|
id: 'server-url'
|
|
});
|
|
|
|
if ( this.existing ) {
|
|
this.forceNamespace = this.existing.metadata.namespace;
|
|
this.nameDisabled = true;
|
|
} else if (this.$route.query[FROM_CLUSTER] === _FLAGGED) {
|
|
this.forceNamespace = DEFAULT_WORKSPACE;
|
|
} else if ( this.chart?.targetNamespace ) {
|
|
this.forceNamespace = this.chart.targetNamespace;
|
|
} else if ( this.query.appNamespace ) {
|
|
this.forceNamespace = this.query.appNamespace;
|
|
} else {
|
|
this.forceNamespace = null;
|
|
}
|
|
|
|
this.value = await this.$store.dispatch('cluster/create', {
|
|
type: 'chartInstallAction',
|
|
metadata: {
|
|
namespace: this.forceNamespace || this.$store.getters['defaultNamespace'],
|
|
name: this.existing?.spec?.name || this.query.appName || '',
|
|
}
|
|
});
|
|
|
|
if ( !this.existing) {
|
|
if ( this.chart?.targetName ) {
|
|
this.value.metadata.name = this.chart.targetName;
|
|
this.nameDisabled = true;
|
|
} else if ( this.query.appName ) {
|
|
this.value.metadata.name = this.query.appName;
|
|
} else {
|
|
this.nameDisabled = false;
|
|
}
|
|
|
|
if ( this.query.description ) {
|
|
this.value.setAnnotation(DESCRIPTION_ANNOTATION, this.query.description);
|
|
}
|
|
}
|
|
|
|
if (this.forceNamespace && !this.existing) {
|
|
let ns;
|
|
|
|
try {
|
|
ns = await this.$store.dispatch('cluster/find', { type: NAMESPACE, id: this.forceNamespace });
|
|
const project = ns.metadata.annotations[PROJECT];
|
|
|
|
if (project) {
|
|
this.project = project.replace(':', '/');
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
if ( !this.chart || !this.query.versionName) {
|
|
return;
|
|
}
|
|
|
|
if ( this.version && process.client ) {
|
|
await this.loadValuesComponent();
|
|
}
|
|
|
|
await this.loadChartSteps();
|
|
|
|
if ( !this.loadedVersion || this.loadedVersion !== this.version.key ) {
|
|
let userValues;
|
|
|
|
if ( this.loadedVersion ) {
|
|
// If changing charts once the page is loaded, diff from the chart you were
|
|
// previously on to get the actual customization, then apply onto the new chart values.
|
|
|
|
if ( this.showingYaml ) {
|
|
this.applyYamlToValues();
|
|
}
|
|
|
|
userValues = diff(this.loadedVersionValues, this.chartValues);
|
|
} else if ( this.existing ) {
|
|
// For an existing app, use the values from the previous install
|
|
userValues = clone(this.existing.spec?.values || {});
|
|
// For an existing app, use the values from the previous install
|
|
} else {
|
|
// For an new app, start empty
|
|
userValues = {};
|
|
}
|
|
|
|
this.removeGlobalValuesFrom(userValues);
|
|
this.chartValues = merge(merge({}, this.versionInfo?.values || {}), userValues);
|
|
this.valuesYaml = saferDump(this.chartValues);
|
|
|
|
// For YAML diff
|
|
if ( !this.loadedVersion ) {
|
|
this.originalYamlValues = this.valuesYaml;
|
|
}
|
|
|
|
this.loadedVersionValues = this.versionInfo?.values || {};
|
|
this.loadedVersion = this.version?.key;
|
|
}
|
|
|
|
this.updateStepOneReady();
|
|
|
|
this.preFormYamlOption = this.valuesComponent || this.hasQuestions ? VALUES_STATE.FORM : VALUES_STATE.YAML;
|
|
|
|
this.reademeWindowName = `${ this.stepperName }-${ this.version?.version }`;
|
|
},
|
|
|
|
data() {
|
|
const defaultCmdOpts = {
|
|
cleanupOnFail: false,
|
|
crds: true,
|
|
hooks: true,
|
|
force: false,
|
|
resetValues: false,
|
|
openApi: true,
|
|
wait: true,
|
|
timeout: 600,
|
|
historyMax: 5,
|
|
};
|
|
|
|
return {
|
|
defaultRegistrySetting: null,
|
|
serverUrlSetting: null,
|
|
chartValues: null,
|
|
originalYamlValues: null,
|
|
previousYamlValues: null,
|
|
errors: null,
|
|
existing: null,
|
|
forceNamespace: null,
|
|
loadedVersion: null,
|
|
loadedVersionValues: null,
|
|
mode: null,
|
|
value: null,
|
|
valuesComponent: null,
|
|
valuesYaml: '',
|
|
project: null,
|
|
reademeWindowName: null,
|
|
|
|
defaultCmdOpts,
|
|
customCmdOpts: { ...defaultCmdOpts },
|
|
|
|
nameDisabled: false,
|
|
|
|
preFormYamlOption: VALUES_STATE.YAML,
|
|
formYamlOption: VALUES_STATE.YAML,
|
|
showDiff: false,
|
|
showValuesComponent: true,
|
|
showQuestions: true,
|
|
showSlideIn: false,
|
|
componentHasTabs: false,
|
|
showCommandStep: false,
|
|
isNamespaceNew: false,
|
|
|
|
stepBasic: {
|
|
name: 'basics',
|
|
label: this.t('catalog.install.steps.basics.label'),
|
|
subtext: this.t('catalog.install.steps.basics.subtext'),
|
|
ready: true,
|
|
weight: 30
|
|
},
|
|
stepValues: {
|
|
name: 'helmValues',
|
|
label: this.t('catalog.install.steps.helmValues.label'),
|
|
subtext: this.t('catalog.install.steps.helmValues.subtext'),
|
|
ready: true,
|
|
weight: 20
|
|
},
|
|
stepCommands: {
|
|
name: 'helmCli',
|
|
label: this.t('catalog.install.steps.helmCli.label'),
|
|
subtext: this.t('catalog.install.steps.helmCli.subtext'),
|
|
ready: true,
|
|
weight: 10
|
|
},
|
|
|
|
customSteps: [
|
|
|
|
]
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
namespaceIsNew() {
|
|
const all = this.$store.getters['cluster/all'](NAMESPACE);
|
|
const want = this.value?.metadata?.namespace;
|
|
|
|
if ( !want ) {
|
|
return false;
|
|
}
|
|
|
|
return !findBy(all, 'id', want);
|
|
},
|
|
|
|
showProject() {
|
|
return this.isRancher && !this.existing && this.forceNamespace;
|
|
},
|
|
|
|
projectOpts() {
|
|
const cluster = this.currentCluster;
|
|
const projects = this.$store.getters['management/all'](MANAGEMENT.PROJECT);
|
|
|
|
const out = projects.filter(x => x.spec.clusterName === cluster.id).map((project) => {
|
|
return {
|
|
id: project.id,
|
|
label: project.nameDisplay,
|
|
value: project.id
|
|
};
|
|
});
|
|
|
|
out.unshift({
|
|
id: 'none',
|
|
label: '(None)',
|
|
value: '',
|
|
});
|
|
|
|
return out;
|
|
},
|
|
|
|
charts() {
|
|
const current = this.existing?.matchingChart(true);
|
|
|
|
const out = this.$store.getters['catalog/charts'].filter((x) => {
|
|
if ( x.key === current?.key || x.chartName === current?.chartName ) {
|
|
return true;
|
|
}
|
|
|
|
if ( x.hidden && !this.showHidden ) {
|
|
return false;
|
|
}
|
|
|
|
if ( x.deprecated && !this.showDeprecated ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
let last = '';
|
|
|
|
for ( let i = 0 ; i < out.length ; i++ ) {
|
|
if ( out[i].repoName !== last ) {
|
|
last = out[i].repoName;
|
|
insertAt(out, i, {
|
|
kind: 'label',
|
|
label: out[i].repoNameDisplay,
|
|
disabled: true
|
|
});
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return out;
|
|
},
|
|
|
|
showNameEditor() {
|
|
return !this.nameDisabled || !this.forceNamespace;
|
|
},
|
|
|
|
showVersions() {
|
|
return this.chart?.versions.length > 1;
|
|
},
|
|
|
|
targetNamespace() {
|
|
if ( this.forceNamespace ) {
|
|
return this.forceNamespace;
|
|
} else if ( this.value?.metadata.namespace ) {
|
|
return this.value.metadata.namespace;
|
|
}
|
|
|
|
return 'default';
|
|
},
|
|
|
|
editorMode() {
|
|
if ( this.showDiff ) {
|
|
return EDITOR_MODES.DIFF_CODE;
|
|
}
|
|
|
|
return EDITOR_MODES.EDIT_CODE;
|
|
},
|
|
|
|
showingYaml() {
|
|
return this.formYamlOption === VALUES_STATE.YAML || ( !this.valuesComponent && !this.hasQuestions );
|
|
},
|
|
|
|
formYamlOptions() {
|
|
const options = [];
|
|
|
|
if (this.valuesComponent || this.hasQuestions) {
|
|
options.push({
|
|
labelKey: 'catalog.install.section.chartOptions',
|
|
value: VALUES_STATE.FORM,
|
|
});
|
|
}
|
|
options.push({
|
|
labelKey: 'catalog.install.section.valuesYaml',
|
|
value: VALUES_STATE.YAML,
|
|
}, {
|
|
labelKey: 'catalog.install.section.diff',
|
|
value: VALUES_STATE.DIFF,
|
|
// === quite obviously shouldn't work, but has been and still does. When the magic breaks address with heavier stringify/jsyaml.safeDump
|
|
disabled: this.formYamlOption === VALUES_STATE.FORM ? this.originalYamlValues === jsyaml.safeDump(this.chartValues || {}) : this.originalYamlValues === this.valuesYaml,
|
|
});
|
|
|
|
return options;
|
|
},
|
|
|
|
yamlDiffModeOptions() {
|
|
return [{
|
|
labelKey: 'resourceYaml.buttons.unified',
|
|
value: 'unified',
|
|
}, {
|
|
labelKey: 'resourceYaml.buttons.split',
|
|
value: 'split',
|
|
}];
|
|
},
|
|
|
|
stepperName() {
|
|
return this.existing?.nameDisplay || this.chart?.chartNameDisplay;
|
|
},
|
|
|
|
stepperSubtext() {
|
|
return this.existing && this.currentVersion !== this.targetVersion ? `${ this.currentVersion } > ${ this.targetVersion }` : this.targetVersion;
|
|
},
|
|
|
|
showingReadmeWindow() {
|
|
return !!this.$store.getters['wm/byId'](this.reademeWindowName);
|
|
},
|
|
|
|
diffMode: mapPref(DIFF),
|
|
|
|
step1Description() {
|
|
return this.$store.getters['i18n/withFallback']('catalog.install.steps.basics.description', { action: this.action, existing: !!this.existing }, '');
|
|
},
|
|
|
|
step2Description() {
|
|
return this.$store.getters['i18n/withFallback']('catalog.install.steps.helmValues.description', { action: this.action, existing: !!this.existing }, '');
|
|
},
|
|
|
|
step3Description() {
|
|
return this.$store.getters['i18n/withFallback']('catalog.install.steps.helmCli.description', { action: this.action, existing: !!this.existing }, '');
|
|
},
|
|
|
|
steps() {
|
|
let steps;
|
|
|
|
const type = this.version?.annotations?.[CATALOG_ANNOTATIONS.TYPE];
|
|
|
|
if ( type === CATALOG_ANNOTATIONS._CLUSTER_TPL ) {
|
|
steps = [this.stepValues];
|
|
} else {
|
|
steps = [
|
|
this.stepBasic,
|
|
this.stepValues,
|
|
...this.customSteps
|
|
];
|
|
}
|
|
|
|
if (this.showCommandStep) {
|
|
steps.push(this.stepCommands);
|
|
}
|
|
|
|
return steps.sort((a, b) => (b.weight || 0) - (a.weight || 0));
|
|
},
|
|
|
|
cmdOptions() {
|
|
return this.showCommandStep ? this.customCmdOpts : this.defaultCmdOpts;
|
|
},
|
|
|
|
namespaceNewAllowed() {
|
|
return !this.existing && !this.forceNamespace;
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
'$route.query'(neu, old) {
|
|
if ( !isEqual(neu, old) ) {
|
|
this.$fetch();
|
|
}
|
|
},
|
|
|
|
'value.metadata.namespace'(neu, old) {
|
|
if (neu) {
|
|
const ns = this.$store.getters['cluster/byId'](NAMESPACE, this.value.metadata.namespace);
|
|
|
|
const project = ns?.metadata.annotations[PROJECT];
|
|
|
|
if (project) {
|
|
this.project = project.replace(':', '/');
|
|
}
|
|
}
|
|
},
|
|
|
|
preFormYamlOption(neu, old) {
|
|
if (neu === VALUES_STATE.FORM && this.valuesYaml !== this.previousYamlValues && !!this.$refs.cancelModal) {
|
|
this.$refs.cancelModal.show();
|
|
} else {
|
|
this.formYamlOption = neu;
|
|
}
|
|
},
|
|
|
|
formYamlOption(neu, old) {
|
|
switch (neu) {
|
|
case VALUES_STATE.FORM:
|
|
// Return to form, reset everything back to starting point
|
|
this.valuesYaml = this.previousYamlValues;
|
|
|
|
this.showValuesComponent = true;
|
|
this.showQuestions = true;
|
|
|
|
this.showDiff = false;
|
|
break;
|
|
case VALUES_STATE.YAML:
|
|
// Show the YAML preview
|
|
if (old === VALUES_STATE.FORM) {
|
|
this.valuesYaml = jsyaml.safeDump(this.chartValues || {});
|
|
this.previousYamlValues = this.valuesYaml;
|
|
}
|
|
|
|
this.showValuesComponent = false;
|
|
this.showQuestions = false;
|
|
|
|
this.showDiff = false;
|
|
break;
|
|
case VALUES_STATE.DIFF:
|
|
// Show the YAML diff
|
|
if (old === VALUES_STATE.FORM) {
|
|
this.valuesYaml = jsyaml.safeDump(this.chartValues || {});
|
|
this.previousYamlValues = this.valuesYaml;
|
|
}
|
|
|
|
this.showValuesComponent = false;
|
|
this.showQuestions = false;
|
|
|
|
this.showDiff = true;
|
|
break;
|
|
}
|
|
},
|
|
|
|
requires() {
|
|
this.updateStepOneReady();
|
|
},
|
|
|
|
warnings() {
|
|
this.updateStepOneReady();
|
|
},
|
|
|
|
},
|
|
|
|
async mounted() {
|
|
await this.loadValuesComponent();
|
|
|
|
await this.loadChartSteps();
|
|
|
|
window.scrollTop = 0;
|
|
|
|
// For easy access debugging...
|
|
if ( typeof window !== 'undefined' ) {
|
|
window.v = this.value;
|
|
window.c = this;
|
|
}
|
|
|
|
this.preFormYamlOption = this.valuesComponent || this.hasQuestions ? VALUES_STATE.FORM : VALUES_STATE.YAML;
|
|
},
|
|
|
|
beforeDestroy() {
|
|
this.$store.dispatch('wm/close', this.reademeWindowName, { root: true });
|
|
},
|
|
|
|
methods: {
|
|
|
|
async loadValuesComponent() {
|
|
// TODO: Remove RELEASE_NAME. This is only in until the component annotation is added to the OPA Gatekeeper chart
|
|
const component = this.version?.annotations?.[CATALOG_ANNOTATIONS.COMPONENT] || this.version?.annotations?.[CATALOG_ANNOTATIONS.RELEASE_NAME];
|
|
|
|
if ( component ) {
|
|
if ( this.$store.getters['catalog/haveComponent'](component) ) {
|
|
this.valuesComponent = this.$store.getters['catalog/importComponent'](component);
|
|
const loaded = await this.valuesComponent();
|
|
|
|
this.showValuesComponent = true;
|
|
this.componentHasTabs = loaded?.default?.hasTabs || false;
|
|
} else {
|
|
this.valuesComponent = null;
|
|
this.componentHasTabs = false;
|
|
this.showValuesComponent = false;
|
|
}
|
|
} else {
|
|
this.valuesComponent = null;
|
|
this.componentHasTabs = false;
|
|
this.showValuesComponent = false;
|
|
}
|
|
},
|
|
|
|
async loadChartSteps() {
|
|
const component = this.version?.annotations?.[CATALOG_ANNOTATIONS.COMPONENT] || this.version?.annotations?.[CATALOG_ANNOTATIONS.RELEASE_NAME];
|
|
|
|
if ( component ) {
|
|
const steps = await this.$store.getters['catalog/chartSteps'](component);
|
|
|
|
this.customSteps = await Promise.all( steps.map(cs => this.loadChartStep(cs)));
|
|
}
|
|
},
|
|
|
|
async loadChartStep(customStep) {
|
|
const loaded = await customStep.component();
|
|
const withFallBack = this.$store.getters['i18n/withFallback'];
|
|
|
|
return {
|
|
name: customStep.name,
|
|
label: withFallBack(loaded?.default?.label, null, customStep.name),
|
|
subtext: withFallBack(loaded?.default?.subtext, null, ''),
|
|
weight: loaded?.default?.weight,
|
|
ready: false,
|
|
hidden: true,
|
|
loading: true,
|
|
component: customStep.component,
|
|
};
|
|
},
|
|
|
|
selectChart(chart) {
|
|
if ( !chart ) {
|
|
return;
|
|
}
|
|
|
|
this.$router.applyQuery({
|
|
[REPO]: chart.repoName,
|
|
[REPO_TYPE]: chart.repoType,
|
|
[CHART]: chart.chartName,
|
|
[VERSION]: chart.versions[0].version
|
|
});
|
|
},
|
|
|
|
cancel() {
|
|
if ( this.existing ) {
|
|
this.done();
|
|
} else if (this.$route.query[FROM_TOOLS] === _FLAGGED) {
|
|
this.$router.replace(this.clusterToolsLocation());
|
|
} else if (this.$route.query[FROM_CLUSTER] === _FLAGGED) {
|
|
this.$router.replace(this.clustersLocation());
|
|
} else {
|
|
this.$router.replace(this.chartLocation(false));
|
|
}
|
|
},
|
|
|
|
done() {
|
|
if ( this.$route.query[FROM_TOOLS] === _FLAGGED ) {
|
|
this.$router.replace(this.clusterToolsLocation());
|
|
} else if (this.$route.query[FROM_CLUSTER] === _FLAGGED) {
|
|
this.$router.replace(this.clustersLocation());
|
|
} else {
|
|
// If the create app process fails helm validation then we still get here... so until this is fixed new apps will be taken to the
|
|
// generic apps list (existing apps will be taken to their detail page)
|
|
this.$router.replace(this.appLocation());
|
|
}
|
|
},
|
|
|
|
async finish(btnCb) {
|
|
try {
|
|
const isUpgrade = !!this.existing;
|
|
|
|
this.errors = [];
|
|
|
|
await this.applyHooks(BEFORE_SAVE_HOOKS);
|
|
|
|
const { errors, input } = this.actionInput(isUpgrade);
|
|
|
|
if ( errors?.length ) {
|
|
this.errors = errors;
|
|
btnCb(false);
|
|
|
|
return;
|
|
}
|
|
const res = await this.repo.doAction((isUpgrade ? 'upgrade' : 'install'), input);
|
|
|
|
this.operation = await this.$store.dispatch('cluster/find', {
|
|
type: CATALOG.OPERATION,
|
|
id: `${ res.operationNamespace }/${ res.operationName }`
|
|
});
|
|
|
|
try {
|
|
await this.operation.waitForLink('logs');
|
|
this.operation.openLogs();
|
|
} catch (e) {
|
|
// The wait times out eventually, move on...
|
|
}
|
|
|
|
await this.applyHooks(AFTER_SAVE_HOOKS);
|
|
|
|
btnCb(true);
|
|
this.done();
|
|
} catch (err) {
|
|
this.errors = exceptionToErrorsArray(err);
|
|
btnCb(false);
|
|
}
|
|
},
|
|
|
|
addGlobalValuesTo(values) {
|
|
let global = values.global;
|
|
|
|
if ( !global ) {
|
|
global = {};
|
|
set(values, 'global', global);
|
|
}
|
|
|
|
let cattle = global.cattle;
|
|
|
|
if ( !cattle ) {
|
|
cattle = {};
|
|
set(values.global, 'cattle', cattle);
|
|
}
|
|
|
|
const cluster = this.currentCluster;
|
|
const defaultRegistry = this.defaultRegistrySetting?.value || '';
|
|
const serverUrl = this.serverUrlSetting?.value || '';
|
|
const isWindows = cluster.providerOs === 'windows';
|
|
const pathPrefix = cluster.spec?.rancherKubernetesEngineConfig?.prefixPath || '';
|
|
const windowsPathPrefix = cluster.spec?.rancherKubernetesEngineConfig?.winPrefixPath || '';
|
|
|
|
setIfNotSet(cattle, 'clusterId', cluster.id);
|
|
setIfNotSet(cattle, 'clusterName', cluster.nameDisplay);
|
|
setIfNotSet(cattle, 'systemDefaultRegistry', defaultRegistry);
|
|
setIfNotSet(global, 'systemDefaultRegistry', defaultRegistry);
|
|
setIfNotSet(cattle, 'url', serverUrl);
|
|
setIfNotSet(cattle, 'rkePathPrefix', pathPrefix);
|
|
setIfNotSet(cattle, 'rkeWindowsPathPrefix', windowsPathPrefix);
|
|
|
|
if ( isWindows ) {
|
|
setIfNotSet(cattle, 'windows.enabled', true);
|
|
}
|
|
|
|
return values;
|
|
|
|
function setIfNotSet(obj, key, val) {
|
|
if ( typeof get(obj, key) === 'undefined' ) {
|
|
set(obj, key, val);
|
|
}
|
|
}
|
|
},
|
|
|
|
removeGlobalValuesFrom(values) {
|
|
if ( !values ) {
|
|
return;
|
|
}
|
|
|
|
const cluster = this.$store.getters['currentCluster'];
|
|
const defaultRegistry = this.defaultRegistrySetting?.value || '';
|
|
const serverUrl = this.serverUrlSetting?.value || '';
|
|
const isWindows = cluster.providerOs === 'windows';
|
|
const pathPrefix = cluster.spec?.rancherKubernetesEngineConfig?.prefixPath || '';
|
|
const windowsPathPrefix = cluster.spec?.rancherKubernetesEngineConfig?.winPrefixPath || '';
|
|
|
|
if ( values.global?.cattle ) {
|
|
deleteIfEqual(values.global.cattle, 'clusterId', cluster.id);
|
|
deleteIfEqual(values.global.cattle, 'clusterName', cluster.nameDisplay);
|
|
deleteIfEqual(values.global.cattle, 'systemDefaultRegistry', defaultRegistry);
|
|
deleteIfEqual(values.global.cattle, 'url', serverUrl);
|
|
deleteIfEqual(values.global.cattle, 'rkePathPrefix', pathPrefix);
|
|
deleteIfEqual(values.global.cattle, 'rkeWindowsPathPrefix', windowsPathPrefix);
|
|
|
|
if ( isWindows ) {
|
|
deleteIfEqual(values.global.cattle.windows, 'enabled', true);
|
|
}
|
|
}
|
|
|
|
if ( values.global?.cattle?.windows && !Object.keys(values.global.cattle.windows).length ) {
|
|
delete values.global.cattle.windows;
|
|
}
|
|
|
|
if ( values.global?.cattle && !Object.keys(values.global.cattle).length ) {
|
|
delete values.global.cattle;
|
|
}
|
|
|
|
if ( values.global ) {
|
|
deleteIfEqual(values.global, 'systemDefaultRegistry', defaultRegistry);
|
|
}
|
|
|
|
if ( !Object.keys(values.global || {}).length ) {
|
|
delete values.global;
|
|
}
|
|
|
|
return values;
|
|
|
|
function deleteIfEqual(obj, key, val) {
|
|
if ( get(obj, key) === val ) {
|
|
delete obj[key];
|
|
}
|
|
}
|
|
},
|
|
|
|
applyYamlToValues() {
|
|
try {
|
|
this.chartValues = jsyaml.safeLoad(this.valuesYaml);
|
|
} catch (err) {
|
|
return { errors: exceptionToErrorsArray(err) };
|
|
}
|
|
|
|
return { errors: [] };
|
|
},
|
|
|
|
actionInput(isUpgrade) {
|
|
const fromChart = this.versionInfo?.values || {};
|
|
|
|
const errors = [];
|
|
|
|
if ( this.showingYaml ) {
|
|
const { errors: yamlErrors } = this.applyYamlToValues();
|
|
|
|
errors.push(...yamlErrors);
|
|
}
|
|
|
|
// Only save the values that differ from the chart's standard values.yaml
|
|
const values = diff(fromChart, this.chartValues);
|
|
|
|
// Add our special blend of 11 herbs and global values
|
|
this.addGlobalValuesTo(values);
|
|
|
|
const form = JSON.parse(JSON.stringify(this.value));
|
|
|
|
const chart = {
|
|
chartName: this.chart.chartName,
|
|
version: this.version?.version || this.query.versionName,
|
|
releaseName: form.metadata.name,
|
|
description: form.metadata?.annotations?.[DESCRIPTION_ANNOTATION],
|
|
annotations: {
|
|
[CATALOG_ANNOTATIONS.SOURCE_REPO_TYPE]: this.chart.repoType,
|
|
[CATALOG_ANNOTATIONS.SOURCE_REPO_NAME]: this.chart.repoName
|
|
},
|
|
values,
|
|
};
|
|
|
|
if ( isUpgrade ) {
|
|
chart.resetValues = this.cmdOptions.resetValues;
|
|
}
|
|
|
|
const out = {
|
|
charts: [chart],
|
|
noHooks: this.cmdOptions.hooks === false,
|
|
timeout: this.cmdOptions.timeout > 0 ? `${ this.cmdOptions.timeout }s` : null,
|
|
wait: this.cmdOptions.wait === true,
|
|
namespace: form.metadata.namespace,
|
|
projectId: this.project,
|
|
};
|
|
|
|
if ( isUpgrade ) {
|
|
out.force = this.cmdOptions.force === true;
|
|
out.historyMax = this.cmdOptions.historyMax;
|
|
out.cleanupOnFail = this.cmdOptions.cleanupOnFail;
|
|
} else {
|
|
out.disableOpenAPIValidation = this.cmdOptions.openApi === false;
|
|
out.skipCRDs = this.cmdOptions.crds === false;
|
|
}
|
|
|
|
const more = [];
|
|
|
|
let auto = (this.version.annotations?.[CATALOG_ANNOTATIONS.AUTO_INSTALL] || '').split(/\s*,\s*/).filter(x => !!x).reverse();
|
|
|
|
for ( const constraint of auto ) {
|
|
const provider = this.$store.getters['catalog/versionSatisfying']({
|
|
constraint,
|
|
repoName: this.chart.repoName,
|
|
repoType: this.chart.repoType,
|
|
chartVersion: this.version.version,
|
|
});
|
|
|
|
if ( provider ) {
|
|
more.push(provider);
|
|
} else {
|
|
errors.push(`This chart requires ${ constraint } but no matching chart was found`);
|
|
}
|
|
}
|
|
|
|
auto = (this.version.annotations?.[CATALOG_ANNOTATIONS.AUTO_INSTALL_GVK] || '').split(/\s*,\s*/).filter(x => !!x).reverse();
|
|
|
|
for ( const gvr of auto ) {
|
|
const provider = this.$store.getters['catalog/versionProviding']({
|
|
gvr,
|
|
repoName: this.chart.repoName,
|
|
repoType: this.chart.repoType
|
|
});
|
|
|
|
if ( provider ) {
|
|
more.push(provider);
|
|
} else {
|
|
errors.push(`This chart requires another chart that provides ${ gvr }, but none was was found`);
|
|
}
|
|
}
|
|
|
|
for ( const dependency of more ) {
|
|
out.charts.unshift({
|
|
chartName: dependency.name,
|
|
version: dependency.version,
|
|
releaseName: dependency.annotations[CATALOG_ANNOTATIONS.RELEASE_NAME] || dependency.name,
|
|
projectId: this.project,
|
|
values: this.addGlobalValuesTo({}),
|
|
annotations: {
|
|
[CATALOG_ANNOTATIONS.SOURCE_REPO_TYPE]: dependency.repoType,
|
|
[CATALOG_ANNOTATIONS.SOURCE_REPO_NAME]: dependency.repoName
|
|
},
|
|
});
|
|
}
|
|
|
|
return { errors, input: out };
|
|
},
|
|
|
|
tabChanged() {
|
|
window.scrollTop = 0;
|
|
},
|
|
|
|
updateStepOneReady() {
|
|
const okRequires = !this.requires.length;
|
|
const okChart = !!this.chart;
|
|
|
|
this.steps[0].ready = okRequires && okChart;
|
|
},
|
|
|
|
getOptionLabel(opt) {
|
|
return opt?.chartNameDisplay;
|
|
},
|
|
|
|
showReadmeWindow() {
|
|
this.$store.dispatch('wm/open', {
|
|
id: this.reademeWindowName,
|
|
label: this.reademeWindowName,
|
|
icon: 'file',
|
|
component: 'ChartReadme',
|
|
attrs: { versionInfo: this.versionInfo }
|
|
}, { root: true });
|
|
},
|
|
|
|
updateStep(stepName, update) {
|
|
const step = this.steps.find(step => step.name === stepName);
|
|
|
|
if (step) {
|
|
for (const prop in update) {
|
|
Vue.set(step, prop, update[prop]);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Loading v-if="$fetchState.pending" />
|
|
<div v-else class="install-steps">
|
|
<Wizard
|
|
v-if="value"
|
|
:steps="steps"
|
|
:errors="errors"
|
|
:edit-first-step="true"
|
|
:banner-title="stepperName"
|
|
:banner-title-subtext="stepperSubtext"
|
|
:finish-mode="action"
|
|
class="wizard"
|
|
@cancel="cancel"
|
|
@finish="finish"
|
|
>
|
|
<template v-for="customStep of customSteps" v-slot:[customStep.name]>
|
|
<component
|
|
:is="customStep.component"
|
|
:key="customStep.name"
|
|
@update="updateStep(customStep.name, $event)"
|
|
@errors="e=>errors.push(...e)"
|
|
/>
|
|
</template>
|
|
<template #bannerTitleImage>
|
|
<div class="logo-bg">
|
|
<LazyImage :src="chart ? chart.icon : ''" class="logo" />
|
|
</div>
|
|
</template>
|
|
<template #basics>
|
|
<div class="step__basic">
|
|
<Banner v-if="step1Description" color="info" class="description">
|
|
<span>{{ step1Description }}</span>
|
|
<span v-if="namespaceNewAllowed" class="mt-10">
|
|
{{ t('catalog.install.steps.basics.nsCreationDescription', {}, true) }}
|
|
</span>
|
|
</Banner>
|
|
<div v-if="requires.length || warnings.length" class="mb-30">
|
|
<Banner v-for="msg in requires" :key="msg" color="error">
|
|
<span v-html="msg" />
|
|
</Banner>
|
|
|
|
<Banner v-for="msg in warnings" :key="msg" color="warning">
|
|
<span v-html="msg" />
|
|
</Banner>
|
|
</div>
|
|
<div v-if="existing" class="row mb-10">
|
|
<div class="col span-4">
|
|
<!-- We have a chart, select a new version -->
|
|
<LabeledSelect
|
|
v-if="chart"
|
|
:label="t('catalog.install.version')"
|
|
:value="query.versionName"
|
|
:options="filteredVersions"
|
|
:selectable="version => !version.disabled"
|
|
@input="selectVersion"
|
|
/>
|
|
<!-- There is no chart, let the user try to select one -->
|
|
<LabeledSelect
|
|
v-else
|
|
:label="t('catalog.install.chart')"
|
|
:value="chart"
|
|
:options="charts"
|
|
:selectable="option => !option.disabled"
|
|
:get-option-label="opt => getOptionLabel(opt)"
|
|
option-key="key"
|
|
@input="selectChart($event)"
|
|
>
|
|
<template v-slot:option="opt">
|
|
<template v-if="opt.kind === 'divider'">
|
|
<hr />
|
|
</template>
|
|
<template v-else-if="opt.kind === 'label'">
|
|
<b style="position: relative; left: -2.5px;">{{ opt.label }}</b>
|
|
</template>
|
|
</template>
|
|
</LabeledSelect>
|
|
</div>
|
|
</div>
|
|
|
|
<NameNsDescription
|
|
v-if="chart"
|
|
v-model="value"
|
|
:mode="mode"
|
|
:name-disabled="nameDisabled"
|
|
:name-required="false"
|
|
:name-ns-hidden="!showNameEditor"
|
|
:force-namespace="forceNamespace"
|
|
:namespace-new-allowed="namespaceNewAllowed"
|
|
:extra-columns="showProject ? ['project'] : []"
|
|
:show-spacer="false"
|
|
:horizontal="false"
|
|
@isNamespaceNew="isNamespaceNew = $event"
|
|
>
|
|
<template v-if="showProject" #project>
|
|
<LabeledSelect
|
|
v-model="project"
|
|
:disabled="!namespaceIsNew"
|
|
:label="t('catalog.install.project')"
|
|
option-key="id"
|
|
:options="projectOpts"
|
|
:tooltip="!namespaceIsNew ? t('catalog.install.namespaceIsInProject', {namespace: value.metadata.namespace}, true) : ''"
|
|
:hover-tooltip="!namespaceIsNew"
|
|
:status="'info'"
|
|
/>
|
|
</template>
|
|
</NameNsDescription>
|
|
<div class="step__values__controls--spacer" style="flex:1">
|
|
|
|
</div>
|
|
<Banner v-if="isNamespaceNew" color="info" v-html="t('catalog.install.steps.basics.createNamespace', {namespace: value.metadata.namespace}, true) ">
|
|
</Banner>
|
|
|
|
<Checkbox v-model="showCommandStep" class="mb-20" :label="t('catalog.install.steps.helmCli.checkbox', { action, existing: !!existing })" />
|
|
</div>
|
|
</template>
|
|
<template #helmValues>
|
|
<Banner v-if="step2Description" color="info" class="description">
|
|
{{ step2Description }}
|
|
</Banner>
|
|
<div class="step__values__controls">
|
|
<ButtonGroup
|
|
v-model="preFormYamlOption"
|
|
:options="formYamlOptions"
|
|
inactive-class="bg-disabled btn-sm"
|
|
active-class="bg-primary btn-sm"
|
|
:disabled="preFormYamlOption != formYamlOption"
|
|
></ButtonGroup>
|
|
<div class="step__values__controls--spacer">
|
|
|
|
</div>
|
|
<ButtonGroup
|
|
v-if="showDiff"
|
|
v-model="diffMode"
|
|
:options="yamlDiffModeOptions"
|
|
inactive-class="bg-disabled btn-sm"
|
|
active-class="bg-primary btn-sm"
|
|
></ButtonGroup>
|
|
<div v-if="hasReadme && !showingReadmeWindow" class="btn-group">
|
|
<button type="button" class="btn bg-primary btn-sm" @click="showSlideIn = !showSlideIn">
|
|
{{ t('catalog.install.steps.helmValues.chartInfo.button') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="scroll__container">
|
|
<div class="scroll__content">
|
|
<!-- Values (as Custom Component) -->
|
|
<template v-if="valuesComponent && showValuesComponent">
|
|
<Tabbed
|
|
v-if="componentHasTabs"
|
|
ref="tabs"
|
|
:side-tabs="true"
|
|
:class="{'with-name': showNameEditor}"
|
|
class="step__values__content"
|
|
@changed="tabChanged($event)"
|
|
>
|
|
<component
|
|
:is="valuesComponent"
|
|
v-model="chartValues"
|
|
:mode="mode"
|
|
:chart="chart"
|
|
class="step__values__content"
|
|
:existing="existing"
|
|
:version="version"
|
|
:version-info="versionInfo"
|
|
@warn="e=>errors.push(e)"
|
|
@register-before-hook="registerBeforeHook"
|
|
@register-after-hook="registerAfterHook"
|
|
/>
|
|
</Tabbed>
|
|
<template v-else>
|
|
<component
|
|
:is="valuesComponent"
|
|
v-if="valuesComponent"
|
|
v-model="chartValues"
|
|
:mode="mode"
|
|
:chart="chart"
|
|
class="step__values__content"
|
|
:existing="existing"
|
|
:version="version"
|
|
:version-info="versionInfo"
|
|
@warn="e=>errors.push(e)"
|
|
@register-before-hook="registerBeforeHook"
|
|
@register-after-hook="registerAfterHook"
|
|
/>
|
|
</template>
|
|
</template>
|
|
<!-- Values (as Questions) -->
|
|
<Tabbed
|
|
v-else-if="hasQuestions && showQuestions"
|
|
ref="tabs"
|
|
:side-tabs="true"
|
|
:class="{'with-name': showNameEditor}"
|
|
class="step__values__content"
|
|
@changed="tabChanged($event)"
|
|
>
|
|
<Questions
|
|
v-model="chartValues"
|
|
:mode="mode"
|
|
:source="versionInfo"
|
|
:target-namespace="targetNamespace"
|
|
/>
|
|
</Tabbed>
|
|
<!-- Values (as YAML) -->
|
|
<template v-else>
|
|
<YamlEditor
|
|
ref="yaml"
|
|
v-model="valuesYaml"
|
|
class="step__values__content"
|
|
:scrolling="true"
|
|
:initial-yaml-values="originalYamlValues"
|
|
:editor-mode="editorMode"
|
|
:hide-preview-buttons="true"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Confirm loss of changes on toggle from yaml/preview to form -->
|
|
<ResourceCancelModal ref="cancelModal" :is-cancel-modal="false" :is-form="true" @cancel-cancel="preFormYamlOption=formYamlOption" @confirm-cancel="formYamlOption = preFormYamlOption;"></ResourceCancelModal>
|
|
</template>
|
|
<template #helmCli>
|
|
<Banner v-if="step3Description" color="info" class="description">
|
|
{{ step3Description }}
|
|
</Banner>
|
|
<div><Checkbox v-if="existing" v-model="customCmdOpts.cleanupOnFail" :label="t('catalog.install.helm.cleanupOnFail')" /></div>
|
|
<div><Checkbox v-if="!existing" v-model="customCmdOpts.crds" :label="t('catalog.install.helm.crds')" /></div>
|
|
<div><Checkbox v-model="customCmdOpts.hooks" :label="t('catalog.install.helm.hooks')" /></div>
|
|
<div><Checkbox v-if="existing" v-model="customCmdOpts.force" :label="t('catalog.install.helm.force')" /></div>
|
|
<div><Checkbox v-if="existing" v-model="customCmdOpts.resetValues" :label="t('catalog.install.helm.resetValues')" /></div>
|
|
<div><Checkbox v-if="!existing" v-model="customCmdOpts.openApi" :label="t('catalog.install.helm.openapi')" /></div>
|
|
<div><Checkbox v-model="customCmdOpts.wait" :label="t('catalog.install.helm.wait')" /></div>
|
|
<div style="display: block; max-width: 400px;" class="mt-10">
|
|
<UnitInput
|
|
v-model.number="customCmdOpts.timeout"
|
|
:label="t('catalog.install.helm.timeout.label')"
|
|
:suffix="t('catalog.install.helm.timeout.unit', {value: customCmdOpts.timeout})"
|
|
/>
|
|
</div>
|
|
<div style="display: block; max-width: 400px;" class="mt-10">
|
|
<UnitInput
|
|
v-if="existing"
|
|
v-model.number="customCmdOpts.historyMax"
|
|
:label="t('catalog.install.helm.historyMax.label')"
|
|
:suffix="t('catalog.install.helm.historyMax.unit', {value: customCmdOpts.historyMax})"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Wizard>
|
|
<div class="slideIn" :class="{'hide': false, 'slideIn__show': showSlideIn}">
|
|
<h2 class="slideIn__header">
|
|
{{ t('catalog.install.steps.helmValues.chartInfo.label') }}
|
|
<div class="slideIn__header__buttons">
|
|
<div v-tooltip="t('catalog.install.slideIn.dock')" class="slideIn__header__button" @click="showSlideIn = false; showReadmeWindow()">
|
|
<i class="icon icon-dock" />
|
|
</div>
|
|
<div class="slideIn__header__button" @click="showSlideIn = false">
|
|
<i class="icon icon-close" />
|
|
</div>
|
|
</div>
|
|
</h2>
|
|
<ChartReadme v-if="hasReadme" :version-info="versionInfo" class="chart-content__tabs" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
$title-height: 50px;
|
|
$padding: 5px;
|
|
$slideout-width: 35%;
|
|
|
|
.install-steps {
|
|
position: relative;
|
|
overflow: hidden;
|
|
|
|
.description {
|
|
display: flex;
|
|
flex-direction: column;
|
|
margin-top: 0;
|
|
}
|
|
}
|
|
|
|
.wizard {
|
|
.logo-bg {
|
|
height: $title-height;
|
|
width: $title-height;
|
|
background-color: white;
|
|
border: $padding solid white;
|
|
border-radius: calc( 3 * var(--border-radius));
|
|
position: relative;
|
|
}
|
|
|
|
.logo {
|
|
max-height: $title-height - 2 * $padding;
|
|
max-width: $title-height - 2 * $padding;
|
|
position: absolute;
|
|
width: auto;
|
|
height: auto;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
margin: auto;
|
|
}
|
|
}
|
|
|
|
.step {
|
|
&__basic {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
|
|
.spacer {
|
|
line-height: 2;
|
|
}
|
|
}
|
|
&__values {
|
|
&__controls {
|
|
display: flex;
|
|
margin-bottom: 15px;
|
|
|
|
& > *:not(:last-of-type) {
|
|
margin-right: $padding * 2;
|
|
}
|
|
|
|
&--spacer {
|
|
flex: 1
|
|
}
|
|
|
|
}
|
|
|
|
&__content {
|
|
flex: 1;
|
|
|
|
::v-deep .tab-container {
|
|
overflow: auto;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
.slideIn {
|
|
$slideout-width: 35%;
|
|
|
|
border-left: var(--header-border-size) solid var(--header-border);
|
|
position: absolute;
|
|
top: 0;
|
|
right: -$slideout-width;
|
|
height: 100%;
|
|
background-color: var(--topmenu-bg);
|
|
width: $slideout-width;
|
|
z-index: 10;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
padding: 10px;
|
|
|
|
transition: right .5s ease;
|
|
|
|
&__header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
|
|
&__buttons {
|
|
display: flex;
|
|
}
|
|
|
|
&__button {
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 2px;
|
|
> i {
|
|
font-size: 20px;
|
|
opacity: 0.5;
|
|
}
|
|
&:hover {
|
|
background-color: var(--wm-closer-hover-bg);
|
|
}
|
|
}
|
|
}
|
|
|
|
.chart-content__tabs {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
|
|
height: 0;
|
|
|
|
padding-bottom: 10px;
|
|
|
|
::v-deep .chart-readmes {
|
|
flex: 1;
|
|
overflow: auto;
|
|
}
|
|
}
|
|
|
|
&__show {
|
|
right: 0;
|
|
}
|
|
|
|
}
|
|
|
|
.scroll {
|
|
&__container {
|
|
$yaml-height: 200px;
|
|
display: flex;
|
|
flex: 1;
|
|
min-height: $yaml-height;
|
|
height: 0;
|
|
}
|
|
&__content {
|
|
display: flex;
|
|
flex: 1;
|
|
overflow: auto;
|
|
}
|
|
|
|
}
|
|
|
|
::v-deep .yaml-editor {
|
|
flex: 1
|
|
}
|
|
|
|
</style>
|