dashboard/shell/pages/c/_cluster/apps/charts/install.vue

2051 lines
62 KiB
Vue

<script>
import jsyaml from 'js-yaml';
import merge from 'lodash/merge';
import isEqual from 'lodash/isEqual';
import { mapPref, DIFF } from '@shell/store/prefs';
import { mapFeature, MULTI_CLUSTER, LEGACY } from '@shell/store/features';
import { mapGetters } from 'vuex';
import { markRaw } from 'vue';
import { Banner } from '@components/Banner';
import ButtonGroup from '@shell/components/ButtonGroup';
import ChartReadme from '@shell/components/ChartReadme';
import { Checkbox } from '@components/Form/Checkbox';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
import { LabeledTooltip } from '@components/LabeledTooltip';
import LazyImage from '@shell/components/LazyImage';
import Loading from '@shell/components/Loading';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import ResourceCancelModal from '@shell/components/ResourceCancelModal';
import Questions from '@shell/components/Questions';
import Tabbed from '@shell/components/Tabbed';
import UnitInput from '@shell/components/form/UnitInput';
import YamlEditor, { EDITOR_MODES } from '@shell/components/YamlEditor';
import Wizard from '@shell/components/Wizard';
import TypeDescription from '@shell/components/TypeDescription';
import ChartMixin from '@shell/mixins/chart';
import ChildHook, { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
import { CATALOG, MANAGEMENT, DEFAULT_WORKSPACE, CAPI } from '@shell/config/types';
import {
CHART, FROM_CLUSTER, FROM_TOOLS, HIDE_SIDE_NAV, NAMESPACE, REPO, REPO_TYPE, VERSION, _FLAGGED
} from '@shell/config/query-params';
import { CATALOG as CATALOG_ANNOTATIONS, PROJECT } from '@shell/config/labels-annotations';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { clone, diff, get, set } from '@shell/utils/object';
import { ignoreVariables } from './install.helpers';
import { findBy, insertAt } from '@shell/utils/array';
import { saferDump } from '@shell/utils/create-yaml';
import { LINUX, WINDOWS } from '@shell/store/catalog';
import { SETTING } from '@shell/config/settings';
const VALUES_STATE = {
FORM: 'FORM',
YAML: 'YAML',
DIFF: 'DIFF'
};
function isPlainLayout(query) {
return Object.keys(query).includes(HIDE_SIDE_NAV);
}
export default {
name: 'Install',
layout(context) {
return isPlainLayout(context.query) ? 'plain' : '';
},
components: {
Banner,
ButtonGroup,
ChartReadme,
Checkbox,
LabeledInput,
LabeledSelect,
LabeledTooltip,
LazyImage,
Loading,
NameNsDescription,
ResourceCancelModal,
Questions,
Tabbed,
UnitInput,
YamlEditor,
Wizard,
TypeDescription
},
mixins: [
ChildHook,
ChartMixin
],
async fetch() {
this.errors = [];
// IMPORTANT! Any exception thrown before this.value is set will result in an empty page
/*
fetchChart is defined in shell/mixins. It first checks the URL
query for an app name and namespace. It uses those values to check
for a catalog app resource. If found, it sets the form to edit
mode. If not, it sets the form to create mode.
If the app and app namespace are not provided in the query,
it checks for target name and namespace values defined in the
Helm chart itself.
*/
try {
await this.fetchChart();
} catch (e) {
console.warn('Unable to fetch chart: ', e); // eslint-disable-line no-console
}
try {
await this.fetchAutoInstallInfo();
} catch (e) {
console.warn('Unable to determine if other charts require install: ', e); // eslint-disable-line no-console
}
// If the chart doesn't contain system `systemDefaultRegistry` properties there's no point applying them
if (this.showCustomRegistry) {
// Note: Cluster scoped registry is only supported for node driver clusters
try {
this.clusterRegistry = await this.getClusterRegistry();
} catch (e) {
console.warn('Unable to get cluster registry: ', e); // eslint-disable-line no-console
}
try {
this.globalRegistry = await this.getGlobalRegistry();
} catch (e) {
console.warn('Unable to get global registry: ', e); // eslint-disable-line no-console
}
this.defaultRegistrySetting = this.clusterRegistry || this.globalRegistry;
}
try {
this.serverUrlSetting = await this.$store.dispatch('management/find', {
type: MANAGEMENT.SETTING,
id: SETTING.SERVER_URL,
});
} catch (e) {
console.error('Unable to fetch `server-url` setting: ', e); // eslint-disable-line no-console
}
/*
Figure out the namespace where the chart is
being installed or upgraded.
*/
if ( this.existing ) {
/*
If the Helm chart is already installed,
use the existing namespace by default.
*/
this.forceNamespace = this.existing.metadata.namespace;
this.nameDisabled = true;
} else if (this.$route.query[FROM_CLUSTER] === _FLAGGED) {
/* For Fleet, use the fleet-default namespace. */
this.forceNamespace = DEFAULT_WORKSPACE;
} else if ( this.chart?.targetNamespace ) {
/* If a target namespace is defined in the chart,
set the target namespace as default. */
this.forceNamespace = this.chart.targetNamespace;
} else if ( this.query.appNamespace ) {
/* If a namespace is defined in the URL query,
use that namespace as default. */
this.forceNamespace = this.query.appNamespace;
} else {
this.forceNamespace = null;
}
/* Check if the app is deprecated. */
try {
this.legacyApp = this.existing ? await this.existing.deployedAsLegacy() : false;
} catch (e) {
this.legacyApp = false;
console.warn('Unable to determine if existing install is a legacy app: ', e); // eslint-disable-line no-console
}
/* Check if the app is a multicluster deprecated app.
(Multicluster apps were replaced by Fleet.) */
try {
this.mcapp = this.existing ? await this.existing.deployedAsMultiCluster() : false;
} catch (e) {
this.mcapp = false;
console.warn('Unable to determine if existing install is a mc app: ', e); // eslint-disable-line no-console
}
/* The form state is intialized as a chartInstallAction resource. */
try {
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 || '',
}
});
} catch (e) {
console.error('Unable to create object of type `chartInstallAction`: ', e); // eslint-disable-line no-console
// Nothing's going to work without a `value`. See https://github.com/rancher/dashboard/issues/9452 to handle this and other catches.
return;
}
/* Logic for when the Helm chart is not already installed */
if ( !this.existing) {
/*
The target name is used for Git repos for Fleet.
The target name indicates the name of the cluster
group that the chart is meant to be installed in.
*/
if ( this.chart?.targetName ) {
/*
Set the name of the chartInstallAction
to the name of the cluster group
where the chart should be installed.
*/
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.customCmdOpts.description = this.query.description;
}
} /* End of logic for when chart is already installed */
/*
Logic for what to do if the user is installing
the Helm chart for the first time and a default
namespace has been set.
*/
if (this.forceNamespace && !this.existing) {
let ns;
/*
Before moving forward, check to make sure the
default namespace exists and the logged-in user
has permission to see it.
*/
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 no chart by the given app name and namespace
can be found, or if no version is found, do nothing. */
if ( !this.chart || !this.query.versionName) {
return;
}
if ( this.version ) {
/*
Check if the Helm chart has provided the name
of a Vue component to use for configuring
chart values. If so, load that component.
This will set this.valuesComponent,
this.showValuesComponent.
*/
await this.loadValuesComponent();
}
/*
this.loadedVersion will only be true if you select a non-defalut
option from the "Version" dropdown menu in Apps & Marketplace
when updating a previously installed app.
*/
if ( !this.loadedVersion || this.loadedVersion !== this.version.key ) {
let userValues;
/*
When you select a version, a new chart is loaded. Then
Rancher anticipates that you probably want to port all of your
previously customized, non-default values from the old chart
version to the new chart version, so it applies the previous
chart's customization to the new chart values before
you see the values form on the next page in the workflow.
*/
if ( this.loadedVersion ) {
if ( this.showingYaml ) {
this.applyYamlToValues();
}
/*
this.loadedVersionValues is taken from versionInfo,
which contains everything there is to know about a specific
version of a Helm chart, including all chart values,
chart metadata, a short app README and a more
version-specific README called the chart README.
Here we assume that any difference between the values in
two different Helm chart versions is a "user value," or
a user-selected customization.
*/
userValues = diff(this.loadedVersionValues, this.chartValues);
} else if ( this.existing ) {
await this.existing.fetchValues(); // In theory this has already been called, but do again to be safe
/* For an already installed app, use the values from the previous install. */
userValues = clone(this.existing.values || {});
} else {
/* For an new app, start empty. */
userValues = {};
}
/*
Remove global values if they are identical to
the currently available information about the cluster
and Rancher settings.
Immediately before the Helm chart is installed or
upgraded, the global values are re-added.
*/
this.removeGlobalValuesFrom(userValues);
/*
The merge() method is used to merge two or more objects
starting with the left-most to the right-most to create a
parent mapping object. When two keys are the same, the
generated object will have value for the rightmost key.
In this case, any values in userValues override any
matching values in versionInfo.
*/
this.chartValues = merge(merge({}, this.versionInfo?.values || {}), userValues);
if (this.showCustomRegistry) {
/**
* The input to configure the registry should never be
* shown for third-party charts, which don't have Rancher
* global values.
*/
const existingRegistry = this.chartValues?.global?.systemDefaultRegistry || this.chartValues?.global?.cattle?.systemDefaultRegistry;
delete this.chartValues?.global?.systemDefaultRegistry;
delete this.chartValues?.global?.cattle?.systemDefaultRegistry;
this.customRegistrySetting = existingRegistry || this.defaultRegistrySetting;
this.showCustomRegistryInput = !!this.customRegistrySetting;
}
/* Serializes an object as a YAML document */
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;
}
/* Check if chart exists and if required values exist */
this.updateStepOneReady();
this.preFormYamlOption = this.valuesComponent || this.hasQuestions ? VALUES_STATE.FORM : VALUES_STATE.YAML;
/* Look for annotation to say this app is a legacy migrated app (we look in either place for now) */
this.migratedApp = (this.existing?.spec?.chart?.metadata?.annotations?.[CATALOG_ANNOTATIONS.MIGRATED] === 'true');
},
data() {
/* Helm CLI options that are not persisted on the back end,
but are used for the final install/upgrade operation. */
const defaultCmdOpts = {
cleanupOnFail: false,
crds: true,
hooks: true,
force: false,
resetValues: false,
openApi: true,
wait: true,
timeout: 600,
historyMax: 5,
};
return {
defaultRegistrySetting: '',
customRegistrySetting: '',
serverUrlSetting: null,
chartValues: null,
clusterRegistry: '',
originalYamlValues: null,
previousYamlValues: null,
errors: null,
existing: null,
globalRegistry: '',
forceNamespace: null,
loadedVersion: null,
loadedVersionValues: null,
legacyApp: null,
mcapp: null,
mode: null,
value: null,
valuesComponent: null,
valuesYaml: '',
project: null,
migratedApp: false,
defaultCmdOpts,
customCmdOpts: { ...defaultCmdOpts },
autoInstallInfo: [],
nameDisabled: false,
preFormYamlOption: VALUES_STATE.YAML,
formYamlOption: VALUES_STATE.YAML,
showDiff: false,
showValuesComponent: true,
showQuestions: true,
showSlideIn: false,
shownReadmeWindows: [],
showCommandStep: false,
showCustomRegistryInput: false,
isNamespaceNew: false,
stepBasic: {
name: 'basics',
label: this.t('catalog.install.steps.basics.label'),
subtext: this.t('catalog.install.steps.basics.subtext'),
descriptionKey: 'catalog.install.steps.basics.description',
ready: true,
weight: 30
},
stepClusterTplVersion: {
name: 'clusterTplVersion',
label: this.t('catalog.install.steps.clusterTplVersion.label'),
subtext: this.t('catalog.install.steps.clusterTplVersion.subtext'),
descriptionKey: 'catalog.install.steps.helmValues.description',
ready: true,
weight: 30
},
stepValues: {
name: 'helmValues',
label: this.t('catalog.install.steps.helmValues.label'),
subtext: this.t('catalog.install.steps.helmValues.subtext'),
descriptionKey: 'catalog.install.steps.helmValues.description',
ready: true,
weight: 20
},
stepCommands: {
name: 'helmCli',
label: this.t('catalog.install.steps.helmCli.label'),
subtext: this.t('catalog.install.steps.helmCli.subtext'),
descriptionKey: 'catalog.install.steps.helmCli.description',
ready: true,
weight: 10
},
isPlainLayout: isPlainLayout(this.$route.query),
legacyDefs: {
legacy: this.t('catalog.install.error.legacy.category.legacy'),
mcm: this.t('catalog.install.error.legacy.category.mcm')
}
};
},
computed: {
...mapGetters({ inStore: 'catalog/inStore', features: 'features/get' }),
mcm: mapFeature(MULTI_CLUSTER),
/**
* Return list of variables to filter chart questions
*/
ignoreVariables() {
return ignoreVariables(this.versionInfo);
},
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: `(${ this.t('generic.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;
},
showSelectVersionOrChart() {
// Allow the user to choose a version if the app exists OR they've come from tools
return this.existing || (FROM_TOOLS in this.$route.query);
},
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 );
},
showingYamlDiff() {
return this.formYamlOption === VALUES_STATE.DIFF;
},
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.dump
disabled: this.formYamlOption === VALUES_STATE.FORM ? this.originalYamlValues === jsyaml.dump(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;
},
readmeWindowName() {
// Version can change, so allow multiple WM tabs for different versions
return `${ this.stepperName }-${ this.version?.version }`;
},
showingReadmeWindow() {
return !!this.$store.getters['wm/byId'](this.readmeWindowName);
},
diffMode: mapPref(DIFF),
step1Description() {
const descriptionKey = this.steps.find((s) => s.name === 'basics').descriptionKey;
return this.$store.getters['i18n/withFallback'](descriptionKey, { action: this.action, existing: !!this.existing }, '');
},
step2Description() {
const descriptionKey = this.steps.find((s) => s.name === 'helmValues').descriptionKey;
return this.$store.getters['i18n/withFallback'](descriptionKey, { action: this.action, existing: !!this.existing }, '');
},
step3Description() {
const descriptionKey = this.steps.find((s) => s.name === 'helmCli').descriptionKey;
return this.$store.getters['i18n/withFallback'](descriptionKey, { action: this.action, existing: !!this.existing }, '');
},
steps() {
const steps = [];
const type = this.version?.annotations?.[CATALOG_ANNOTATIONS.TYPE];
if ( type === CATALOG_ANNOTATIONS._CLUSTER_TPL ) {
if (this.filteredVersions?.length > 1) {
steps.push(this.stepClusterTplVersion);
}
steps.push({
...this.stepValues,
label: this.t('catalog.install.steps.clusterTplValues.label'),
subtext: this.t('catalog.install.steps.clusterTplValues.subtext'),
descriptionKey: 'catalog.install.steps.clusterTplValues.description',
});
} else {
steps.push(
this.stepBasic,
this.stepValues,
);
}
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;
},
legacyEnabled() {
// Check for the legacy feature flag in the settings
return this.features(LEGACY);
},
legacyFeatureRoute() {
return {
name: 'c-cluster-product-resource',
params: { product: 'settings', resource: 'management.cattle.io.feature' }
};
},
legacyAppRoute() {
return { name: 'c-cluster-legacy-project' };
},
windowsIncompatible() {
if (this.chart?.windowsIncompatible) {
return this.t('catalog.charts.windowsIncompatible');
}
if (this.versionInfo) {
const incompatibleVersion = !(this.versionInfo?.chart?.annotations?.[CATALOG_ANNOTATIONS.PERMITTED_OS] || LINUX).includes('windows');
if (incompatibleVersion && !this.chart.windowsIncompatible) {
return this.t('catalog.charts.versionWindowsIncompatible');
}
}
return null;
},
/**
* Check if the chart contains `systemDefaultRegistry` properties.
* If not we shouldn't apply the setting, because if the option
* is exposed for third-party Helm charts, it confuses users because
* it shows a private registry setting that is never used
* by the chart they are installing. If not hidden, the setting
* does nothing, and if the user changes it, it will look like
* there is a bug in the UI when it doesn't work, because UI is
* exposing a feature that the chart does not have.
*/
showCustomRegistry() {
const global = this.versionInfo?.values?.global || {};
return global.systemDefaultRegistry !== undefined || global.cattle?.systemDefaultRegistry !== undefined;
},
},
watch: {
'$route.query'(neu, old) {
// If the query changes, refetch the chart
// When going back to app list, the query is empty and we don't want to refetch
if ( !isEqual(neu, old) && Object.keys(neu).length > 0 ) {
this.$fetch();
this.showSlideIn = false;
}
},
'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.dump(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.dump(this.chartValues || {});
this.previousYamlValues = this.valuesYaml;
}
this.showValuesComponent = false;
this.showQuestions = false;
this.updateValue(this.valuesYaml);
this.showDiff = true;
break;
}
},
requires() {
this.updateStepOneReady();
},
warnings() {
this.updateStepOneReady();
},
},
async mounted() {
// Load a Vue component named in the Helm chart
// for editing values
await this.loadValuesComponent();
window.scrollTop = 0;
this.preFormYamlOption = this.valuesComponent || this.hasQuestions ? VALUES_STATE.FORM : VALUES_STATE.YAML;
},
beforeUnmount() {
this.shownReadmeWindows.forEach((name) => this.$store.dispatch('wm/close', name, { root: true }));
},
methods: {
async getClusterRegistry() {
const hasPermissionToSeeProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
if (hasPermissionToSeeProvCluster) {
const mgmCluster = this.$store.getters['currentCluster'];
const provClusterId = mgmCluster?.provClusterId;
let provCluster;
try {
provCluster = provClusterId ? await this.$store.dispatch('management/find', {
type: CAPI.RANCHER_CLUSTER,
id: provClusterId
}) : {};
} catch (e) {
console.error(`Unable to fetch prov cluster '${ provClusterId }': `, e); // eslint-disable-line no-console
}
if (provCluster?.isRke2) { // isRke2 returns true for both RKE2 and K3s clusters.
// If a cluster scoped registry exists,
// it should be used by default.
const clusterRegistry = provCluster.agentConfig?.['system-default-registry'] || '';
if (clusterRegistry) {
return clusterRegistry;
}
}
if (provCluster?.isRke1) {
// For RKE1 clusters, the cluster scoped private registry is on the management
// cluster, not the provisioning cluster.
const rke1Registries = mgmCluster?.spec?.rancherKubernetesEngineConfig?.privateRegistries;
if (rke1Registries?.length > 0) {
const defaultRegistry = rke1Registries.find((registry) => {
return registry.isDefault;
});
return defaultRegistry.url;
}
}
}
},
async getGlobalRegistry() {
// Use the global registry as a fallback.
// If it is an empty string, the container
// runtime will pull images from docker.io.
const globalRegistry = await this.$store.dispatch('management/find', {
type: MANAGEMENT.SETTING,
id: SETTING.SYSTEM_DEFAULT_REGISTRY,
});
return globalRegistry.value;
},
updateValue(value) {
if (this.$refs.yaml) {
this.$refs.yaml.updateValue(value);
}
},
async loadValuesComponent() {
// The const component is a string, for example, 'monitoring'.
const component = this.version?.annotations?.[CATALOG_ANNOTATIONS.COMPONENT];
// Load a values component for the UI if it is named in the Helm chart.
if ( component ) {
const hasChartComponent = this.$store.getters['type-map/hasCustomChart'](component);
if ( hasChartComponent ) {
this.valuesComponent = markRaw(this.$store.getters['type-map/importChart'](component));
this.showValuesComponent = true;
} else {
this.valuesComponent = null;
this.showValuesComponent = false;
}
} else {
this.valuesComponent = null;
this.showValuesComponent = false;
}
},
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);
const operationId = `${ res.operationNamespace }/${ res.operationName }`;
// Non-admins without a cluster won't be able to fetch operations immediately
await this.repo.waitForOperation(operationId);
// Dynamically use store decided when loading catalog (covers standard user case when there's not cluster)
this.operation = await this.$store.dispatch(`${ this.inStore }/find`, {
type: CATALOG.OPERATION,
id: operationId
});
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 projects = this.$store.getters['management/all'](MANAGEMENT.PROJECT);
const systemProjectId = projects.find((p) => p.spec?.displayName === 'System')?.id?.split('/')?.[1] || '';
const serverUrl = this.serverUrlSetting?.value || '';
const isWindows = (cluster?.workerOSs || []).includes(WINDOWS);
const pathPrefix = cluster?.spec?.rancherKubernetesEngineConfig?.prefixPath || '';
const windowsPathPrefix = cluster?.spec?.rancherKubernetesEngineConfig?.winPrefixPath || '';
setIfNotSet(cattle, 'clusterId', cluster?.id);
setIfNotSet(cattle, 'clusterName', cluster?.nameDisplay);
if (this.showCustomRegistry) {
set(cattle, 'systemDefaultRegistry', this.customRegistrySetting);
set(global, 'systemDefaultRegistry', this.customRegistrySetting);
}
setIfNotSet(global, 'cattle.systemProjectId', systemProjectId);
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 serverUrl = this.serverUrlSetting?.value || '';
const isWindows = (cluster?.workerOSs || []).includes(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, '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 ( !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.load(this.valuesYaml);
} catch (err) {
return { errors: exceptionToErrorsArray(err) };
}
return { errors: [] };
},
/*
actionInput determines what values Rancher finally sends
to the backend when installing or upgrading the app. It
injects Rancher-specific values into the chart values.
*/
actionInput(isUpgrade) {
/* Default values defined in the Helm chart itself */
const fromChart = this.versionInfo?.values || {};
const errors = [];
if ( this.showingYaml || this.showingYamlDiff ) {
const { errors: yamlErrors } = this.applyYamlToValues();
errors.push(...yamlErrors);
}
/*
Only save the values that differ from the chart's standard values.yaml.
chartValues is created by applying the user's customized onto
the default chart values.
*/
const values = diff(fromChart, this.chartValues);
/*
Refer to the developer docs at docs/developer/helm-chart-apps.md
for details on what values are injected and where they come from.
*/
this.addGlobalValuesTo(values);
const form = JSON.parse(JSON.stringify(this.value));
/*
Migrated annotations are required to allow a deprecated legacy app to be
upgraded.
*/
const migratedAnnotations = this.migratedApp ? { [CATALOG_ANNOTATIONS.MIGRATED]: 'true' } : {};
const chart = {
chartName: this.chart.chartName,
version: this.version?.version || this.query.versionName,
releaseName: form.metadata.name,
description: this.customCmdOpts.description,
annotations: {
...migratedAnnotations,
[CATALOG_ANNOTATIONS.SOURCE_REPO_TYPE]: this.chart.repoType,
[CATALOG_ANNOTATIONS.SOURCE_REPO_NAME]: this.chart.repoName
},
values,
};
if ( isUpgrade ) {
chart.resetValues = this.cmdOptions.resetValues;
}
/*
Configure Helm CLI options for doing the install or
upgrade operation.
*/
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,
};
/*
Configure Helm CLI options that are specific to
installs or specific to upgrades.
*/
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 = [];
const 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`);
}
}
/* Chart custom UI components have the ability to edit CRD chart values eg gatekeeper-crd has values.enableRuntimeDefaultSeccompProfile
like the main chart, only CRD values that differ from defaults should be sent on install/upgrade
CRDs should be installed with the same global values as the main chart
*/
for (const versionInfo of this.autoInstallInfo) {
// allValues are the values potentially changed in the installation ui: any previously customized values + defaults
// values are default values from the chart
const { allValues, values: crdValues } = versionInfo;
// only save crd values that differ from the defaults defined in chart values.yaml
const customizedCrdValues = diff(crdValues, allValues);
// CRD globals should be overwritten by main chart globals
// we want to avoid including globals present on crd values and not main chart values
// that covers the scenario where a global value was customized on a previous install (and so is present in crd global vals) and the user has reverted it to default on this update (no longer present in main chart global vals)
const crdValuesToInstall = { ...customizedCrdValues, global: values.global };
out.charts.unshift({
chartName: versionInfo.chart.name,
version: versionInfo.chart.version,
releaseName: versionInfo.chart.annotations[CATALOG_ANNOTATIONS.RELEASE_NAME] || chart.name,
projectId: this.project,
values: crdValuesToInstall
});
}
/*
'more' contains additional
charts that may not be CRD charts but are also meant to be installed at
the same time.
*/
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({ global: values.global }),
annotations: {
...migratedAnnotations,
[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;
},
updateStepTwoReady(update) {
this.updateStep('helmValues', { ready: update });
},
getOptionLabel(opt) {
return opt?.chartNameDisplay;
},
showReadmeWindow() {
this.$store.dispatch('wm/open', {
id: this.readmeWindowName,
label: this.readmeWindowName,
icon: 'file',
component: 'ChartReadme',
attrs: { versionInfo: this.versionInfo }
}, { root: true });
this.shownReadmeWindows.push(this.readmeWindowName);
},
updateStep(stepName, update) {
const step = this.steps.find((step) => step.name === stepName);
if (step) {
for (const prop in update) {
step[prop] = update[prop];
}
}
}
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div
v-else-if="!legacyApp && !mcapp"
class="install-steps pt-20"
:class="{ 'isPlainLayout': isPlainLayout}"
>
<TypeDescription resource="chart" />
<Wizard
v-if="value"
:steps="steps"
:errors="errors"
:edit-first-step="true"
:banner-title="stepperName"
:banner-title-subtext="stepperSubtext"
:finish-mode="action"
class="wizard"
:class="{'windowsIncompatible': windowsIncompatible}"
@cancel="cancel"
@finish="finish"
>
<template #bannerTitleImage>
<div>
<div class="logo-bg">
<LazyImage
:src="chart ? chart.icon : ''"
class="logo"
/>
</div>
<label
v-if="windowsIncompatible"
class="os-label"
>
{{ windowsIncompatible }}
</label>
</div>
</template>
<template #basics>
<div class="step__basic">
<Banner
v-if="step1Description"
color="info"
class="description"
>
<div>
<span>{{ step1Description }}</span>
<span
v-if="namespaceNewAllowed"
class="mt-10"
>
{{ t('catalog.install.steps.basics.nsCreationDescription', {}, true) }}
</span>
</div>
</Banner>
<div
v-if="requires.length || warnings.length"
class="mb-15"
>
<Banner
v-for="(msg, i) in requires"
:key="i"
color="error"
>
<span v-clean-html="msg" />
</Banner>
<Banner
v-for="(msg, i) in warnings"
:key="i"
color="warning"
>
<span v-clean-html="msg" />
</Banner>
</div>
<div
v-if="showSelectVersionOrChart"
class="row mb-20"
>
<div class="col span-4">
<!-- We have a chart for the app, let the user select a new version -->
<LabeledSelect
v-if="chart"
:label="t('catalog.install.version')"
:value="query.versionName"
:options="filteredVersions"
:selectable="version => !version.disabled"
@update:value="selectVersion"
/>
<!-- Can't find the chart for the app, 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"
@update:value="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-model:value="value"
:description-hidden="true"
: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:value="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>
<Checkbox
v-model:value="showCommandStep"
class="mb-20"
:label="t('catalog.install.steps.helmCli.checkbox', { action, existing: !!existing })"
/>
<Checkbox
v-if="showCustomRegistry"
v-model:value="showCustomRegistryInput"
class="mb-20"
:label="t('catalog.chart.registry.custom.checkBoxLabel')"
:tooltip="t('catalog.chart.registry.tooltip')"
/>
<div class="row">
<div class="col span-6">
<LabeledInput
v-if="showCustomRegistryInput"
v-model:value="customRegistrySetting"
label-key="catalog.chart.registry.custom.inputLabel"
placeholder-key="catalog.chart.registry.custom.placeholder"
:min-height="30"
/>
</div>
</div>
<div
class="step__values__controls--spacer"
style="flex:1"
>
&nbsp;
</div>
<Banner
v-if="isNamespaceNew && value.metadata.namespace.length"
color="info"
class="namespace-create-banner"
>
<div v-clean-html="t('catalog.install.steps.basics.createNamespace', {namespace: value.metadata.namespace}, true) " />
</Banner>
</div>
</template>
<template #clusterTplVersion>
<Banner
color="info"
class="description"
>
{{ t('catalog.install.steps.clusterTplVersion.description') }}
</Banner>
<div class="row mb-20">
<div class="col span-4">
<LabeledSelect
v-if="chart"
:label="t('catalog.install.version')"
:value="query.versionName"
:options="filteredVersions"
:selectable="version => !version.disabled"
@update:value="selectVersion"
/>
</div>
<div class="step__values__controls--spacer">
&nbsp;
</div>
<div class="btn-group">
<button
type="button"
class="btn bg-primary btn-sm"
:disabled="!hasReadme || showingReadmeWindow"
@click="showSlideIn = !showSlideIn"
>
{{ t('catalog.install.steps.helmValues.chartInfo.button') }}
</button>
</div>
</div>
</template>
<template #helmValues>
<Banner
v-if="step2Description"
color="info"
class="description"
>
{{ step2Description }}
</Banner>
<div class="step__values__controls">
<ButtonGroup
v-model:value="preFormYamlOption"
data-testid="btn-group-options-view"
:options="formYamlOptions"
inactive-class="bg-disabled btn-sm"
active-class="bg-primary btn-sm"
:disabled="preFormYamlOption != formYamlOption"
/>
<div class="step__values__controls--spacer">
&nbsp;
</div>
<ButtonGroup
v-if="showDiff"
v-model:value="diffMode"
:options="yamlDiffModeOptions"
inactive-class="bg-disabled btn-sm"
active-class="bg-primary btn-sm"
/>
<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 in ./shell/charts/) -->
<template v-if="valuesComponent && showValuesComponent">
<component
:is="valuesComponent"
v-if="valuesComponent"
v-model:value="chartValues"
:mode="mode"
:chart="chart"
class="step__values__content"
:existing="existing"
:version="version"
:version-info="versionInfo"
:auto-install-info="autoInstallInfo"
@warn="e=>errors.push(e)"
@register-before-hook="registerBeforeHook"
@register-after-hook="registerAfterHook"
/>
</template>
<!-- Values (as Questions, abstracted component based on question.yaml configuration from repositories) -->
<Tabbed
v-else-if="hasQuestions && showQuestions"
ref="tabs"
:side-tabs="true"
:hide-single-tab="true"
:class="{'with-name': showNameEditor}"
class="step__values__content"
@changed="tabChanged($event)"
>
<Questions
v-model:value="chartValues"
:in-store="inStore"
:mode="mode"
:source="versionInfo"
:ignore-variables="ignoreVariables"
tabbed="multiple"
:target-namespace="targetNamespace"
/>
</Tabbed>
<!-- Values (as YAML) -->
<template v-else>
<YamlEditor
ref="yaml"
v-model:value="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;"
/>
</template>
<template #helmCli>
<Banner
v-if="step3Description"
color="info"
class="description"
>
{{ step3Description }}
</Banner>
<div>
<Checkbox
v-if="existing"
v-model:value="customCmdOpts.cleanupOnFail"
:label="t('catalog.install.helm.cleanupOnFail')"
/>
</div>
<div>
<Checkbox
v-if="!existing"
v-model:value="customCmdOpts.crds"
:label="t('catalog.install.helm.crds')"
/>
</div>
<div>
<Checkbox
v-model:value="customCmdOpts.hooks"
:label="t('catalog.install.helm.hooks')"
/>
</div>
<div>
<Checkbox
v-if="existing"
v-model:value="customCmdOpts.force"
:label="t('catalog.install.helm.force')"
/>
</div>
<div>
<Checkbox
v-if="existing"
v-model:value="customCmdOpts.resetValues"
:label="t('catalog.install.helm.resetValues')"
/>
</div>
<div>
<Checkbox
v-if="!existing"
v-model:value="customCmdOpts.openApi"
:label="t('catalog.install.helm.openapi')"
/>
</div>
<div>
<Checkbox
v-model:value="customCmdOpts.wait"
:label="t('catalog.install.helm.wait')"
/>
</div>
<div
style="display: block; max-width: 400px;"
class="mt-10"
>
<UnitInput
v-model:value="customCmdOpts.timeout"
:label="t('catalog.install.helm.timeout.label')"
:suffix="t('catalog.install.helm.timeout.unit', {value: customCmdOpts.timeout})"
type="number"
/>
</div>
<div
style="display: block; max-width: 400px;"
class="mt-10"
>
<UnitInput
v-if="existing"
v-model:value="customCmdOpts.historyMax"
:label="t('catalog.install.helm.historyMax.label')"
:suffix="t('catalog.install.helm.historyMax.unit', {value: customCmdOpts.historyMax})"
type="number"
/>
</div>
<div
style="display: block; max-width: 400px;"
class="mt-10"
>
<LabeledInput
v-model:value="customCmdOpts.description"
label-key="catalog.install.helm.description.label"
placeholder-key="catalog.install.helm.description.placeholder"
:min-height="30"
/>
</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-clean-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>
<!-- App is deployed as a Legacy or MultiCluster app, don't let user update from here -->
<div
v-else
class="install-steps"
:class="{ 'isPlainLayout': isPlainLayout}"
>
<div class="outer-container">
<div class="header mb-20">
<div class="title">
<div class="top choice-banner">
<div class="title">
<!-- Logo -->
<slot name="bannerTitleImage">
<div class="round-image">
<LazyImage
:src="chart ? chart.icon : ''"
class="logo"
/>
</div>
</slot>
<!-- Title with subtext -->
<div class="subtitle">
<h2 v-if="stepperName">
{{ stepperName }}
</h2>
<span
v-if="stepperSubtext"
class="subtext"
>{{ stepperSubtext }}</span>
</div>
</div>
</div>
</div>
</div>
<Banner
color="warning"
class="description"
>
<span v-if="!mcapp">
{{ t('catalog.install.error.legacy.label', { legacyType: mcapp ? legacyDefs.mcm : legacyDefs.legacy }, true) }}
</span>
<template v-if="!legacyEnabled">
<span v-clean-html="t('catalog.install.error.legacy.enableLegacy.prompt', true)" />
<router-link :to="legacyFeatureRoute">
{{ t('catalog.install.error.legacy.enableLegacy.goto') }}
</router-link>
</template>
<template v-else-if="mcapp">
<span v-clean-html="t('catalog.install.error.legacy.mcmNotSupported')" />
</template>
<template v-else>
<router-link :to="legacyAppRoute">
<span v-clean-html="t('catalog.install.error.legacy.navigate')" />
</router-link>
</template>
</Banner>
</div>
</div>
</template>
<style lang="scss" scoped>
$title-height: 50px;
$padding: 5px;
$slideout-width: 35%;
.install-steps {
padding-top: 0;
height: 0;
position: relative;
overflow: hidden;
&.isPlainLayout {
padding: 20px;
}
.description {
display: flex;
flex-direction: column;
margin-top: 0;
}
}
.wizard {
.logo-bg {
margin-right: 10px;
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;
}
// Hack - We're adding an absolute tag under the logo that we want to consume space without breaking vertical alignment of row.
// W ith the slots available this isn't possible without adding tag specific styles to the root wizard classes
&.windowsIncompatible {
:deep() .header {
padding-bottom: 15px;
}
}
.os-label {
position: absolute;
background-color: var(--warning-banner-bg);
color:var(--warning);
margin-top: 5px;
}
}
.step {
&__basic {
display: flex;
flex-direction: column;
flex: 1;
.spacer {
line-height: 2;
}
.namespace-create-banner {
margin-bottom: 70px;
}
}
&__values {
&__controls {
display: flex;
margin-bottom: 15px;
& > *:not(:last-of-type) {
margin-right: $padding * 2;
}
&--spacer {
flex: 1
}
}
&__content {
flex: 1;
: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;
:deep() .chart-readmes {
flex: 1;
overflow: auto;
}
}
&__show {
right: 0;
}
}
.scroll {
&__container {
$yaml-height: 200px;
min-height: $yaml-height;
margin-bottom: 60px;
overflow: auto;
display: flex;
flex: 1;
}
&__content {
display: flex;
flex: 1;
overflow: auto;
}
}
:deep() .yaml-editor {
flex: 1
}
.outer-container {
display: flex;
flex-direction: column;
padding: 0;
overflow: auto;
}
.header {
display: flex;
align-content: space-between;
align-items: center;
border-bottom: var(--header-border-size) solid var(--header-border);
& > .title {
flex: 1;
min-height: 75px;
}
.choice-banner {
flex-basis: 40%;
display: flex;
align-items: center;
&.top {
H2 {
margin: 0px;
}
.title{
display: flex;
align-items: center;
justify-content: space-evenly;
& > .subtitle {
margin: 0 20px;
}
}
.subtitle{
display: flex;
flex-direction: column;
& .subtext {
color: var(--input-label);
}
}
}
&:not(.top){
box-shadow: 0px 0px 12px 3px var(--box-bg);
flex-direction: row;
align-items: center;
justify-content: start;
&:hover{
outline: var(--outline-width) solid var(--outline);
cursor: pointer;
}
}
& .round-image {
min-width: 50px;
height: 50px;
margin: 10px 10px 10px 0;
border-radius: 50%;
overflow: hidden;
.logo {
min-width: 50px;
height: 50px;
}
}
}
}
</style>