dashboard/edit/provisioning.cattle.io.cluster/rke2.vue

1423 lines
43 KiB
Vue

<script>
import difference from 'lodash/difference';
import throttle from 'lodash/throttle';
import isArray from 'lodash/isArray';
import merge from 'lodash/merge';
import { mapGetters } from 'vuex';
import CreateEditView from '@/mixins/create-edit-view';
import { CAPI, MANAGEMENT, NORMAN } from '@/config/types';
import { _CREATE, _EDIT } from '@/config/query-params';
import { DEFAULT_WORKSPACE } from '@/models/provisioning.cattle.io.cluster';
import { findBy, removeObject, clear } from '@/utils/array';
import { clone, diff, isEmpty, set } from '@/utils/object';
import { allHash } from '@/utils/promise';
import { sortBy } from '@/utils/sort';
import { camelToTitle, nlToBr } from '@/utils/string';
import { compare, sortable } from '@/utils/version';
import ArrayList from '@/components/form/ArrayList';
import ArrayListGrouped from '@/components/form/ArrayListGrouped';
import BadgeState from '@/components/BadgeState';
import Banner from '@/components/Banner';
import Checkbox from '@/components/form/Checkbox';
import CruResource from '@/components/CruResource';
import LabeledInput from '@/components/form/LabeledInput';
import LabeledSelect from '@/components/form/LabeledSelect';
import Loading from '@/components/Loading';
import MatchExpressions from '@/components/form/MatchExpressions';
import NameNsDescription from '@/components/form/NameNsDescription';
import RadioGroup from '@/components/form/RadioGroup';
import Tab from '@/components/Tabbed/Tab';
import Tabbed from '@/components/Tabbed';
import UnitInput from '@/components/form/UnitInput';
import YamlEditor from '@/components/YamlEditor';
import Questions from '@/components/Questions';
import { normalizeName } from '@/components/form/NameNsDescription.vue';
import ClusterMembershipEditor from '@/components/form/Members/ClusterMembershipEditor';
import { LEGACY } from '@/store/features';
import semver from 'semver';
import ACE from './ACE';
import AgentEnv from './AgentEnv';
import DrainOptions from './DrainOptions';
import Labels from './Labels';
import MachinePool from './MachinePool';
import RegistryConfigs from './RegistryConfigs';
import RegistryMirrors from './RegistryMirrors';
import S3Config from './S3Config';
import SelectCredential from './SelectCredential';
export default {
components: {
ACE,
AgentEnv,
ArrayList,
ArrayListGrouped,
BadgeState,
Banner,
Checkbox,
ClusterMembershipEditor,
CruResource,
DrainOptions,
LabeledInput,
LabeledSelect,
Labels,
Loading,
MachinePool,
MatchExpressions,
NameNsDescription,
Questions,
RadioGroup,
RegistryConfigs,
RegistryMirrors,
S3Config,
SelectCredential,
Tab,
Tabbed,
UnitInput,
YamlEditor,
},
mixins: [CreateEditView],
props: {
mode: {
type: String,
required: true,
},
value: {
type: Object,
required: true,
},
provider: {
type: String,
required: true,
},
},
async fetch() {
if ( !this.rke2Versions ) {
const hash = {
rke2Versions: this.$store.dispatch('management/request', { url: '/v1-rke2-release/releases' }),
k3sVersions: this.$store.dispatch('management/request', { url: '/v1-k3s-release/releases' }),
};
if ( this.$store.getters['management/schemaFor'](MANAGEMENT.POD_SECURITY_POLICY_TEMPLATE) ) {
hash.allPSPs = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.POD_SECURITY_POLICY_TEMPLATE });
}
const res = await allHash(hash);
this.allPSPs = res.allPSPs || [];
this.rke2Versions = res.rke2Versions.data || [];
this.k3sVersions = res.k3sVersions.data || [];
if ( !this.rke2Versions.length && !this.k3sVersions.length ) {
throw new Error('No version info found in KDM');
}
}
if ( !this.value.spec ) {
set(this.value, 'spec', {});
}
if ( !this.value.spec.machineSelectorConfig ) {
set(this.value.spec, 'machineSelectorConfig', []);
}
if ( !this.value.spec.machineSelectorConfig.find(x => !x.machineLabelSelector) ) {
this.value.spec.machineSelectorConfig.unshift({ config: {} });
}
if ( this.value.spec.cloudCredentialSecretName ) {
this.credentialId = `${ this.value.spec.cloudCredentialSecretName }`;
}
if ( !this.value.spec.kubernetesVersion ) {
const option = this.versionOptions.find(x => !!x.value);
set(this.value.spec, 'kubernetesVersion', option.value);
}
for ( const k in this.serverArgs ) {
if ( this.serverConfig[k] === undefined ) {
const def = this.serverArgs[k].default;
set(this.serverConfig, k, (def !== undefined ? def : undefined));
}
}
for ( const k in this.agentArgs ) {
if ( this.agentConfig[k] === undefined ) {
const def = this.agentArgs[k].default;
set(this.agentConfig, k, (def !== undefined ? def : undefined));
}
}
if ( !this.serverConfig.profile ) {
set(this.serverConfig, 'profile', null);
}
if ( this.rkeConfig.etcd?.s3?.bucket ) {
this.s3Backup = true;
}
if ( !this.rkeConfig.etcd ) {
set(this.rkeConfig, 'etcd', {
disableSnapshots: false,
s3: null,
snapshotRetention: 5,
snapshotScheduleCron: '0 */5 * * *',
});
} else if (typeof this.rkeConfig.etcd.disableSnapshots === 'undefined') {
const disableSnapshots = !this.rkeConfig.etcd.snapshotRetention && !this.rkeConfig.etcd.snapshotScheduleCron;
set(this.rkeConfig.etcd, 'disableSnapshots', disableSnapshots);
}
if ( !this.machinePools ) {
await this.initMachinePools(this.value.spec.rkeConfig.machinePools);
if ( this.mode === _CREATE && !this.machinePools.length ) {
await this.addMachinePool();
}
}
for ( const v of this.addonVersions ) {
if ( this.versionInfo[v.name] ) {
continue;
}
const res = await this.$store.dispatch('catalog/getVersionInfo', {
repoType: 'cluster',
repoName: v.repoName,
chartName: v.name,
versionName: v.version
});
this.versionInfo[v.name] = res;
const fromChart = res.values || {};
const fromUser = this.chartValues[v.name] || this.value.spec.rkeConfig.chartValues[v.name] || {};
const merged = merge(merge({}, fromChart), fromUser);
set(this.chartValues, v.name, merged);
}
if ( this.value.spec.defaultPodSecurityPolicyTemplateName === undefined ) {
set(this.value.spec, 'defaultPodSecurityPolicyTemplateName', '');
}
},
data() {
if ( !this.value.spec.rkeConfig ) {
set(this.value.spec, 'rkeConfig', {});
}
if ( !this.value.spec.rkeConfig.chartValues ) {
set(this.value.spec.rkeConfig, 'chartValues', {});
}
if ( !this.value.spec.rkeConfig.upgradeStrategy ) {
set(this.value.spec.rkeConfig, 'upgradeStrategy', {
controlPlaneConcurrency: '10%',
controlPlaneDrainOptions: {},
workerConcurrency: '10%',
workerDrainOptions: {},
});
}
if ( !this.value.spec.rkeConfig.machineGlobalConfig ) {
set(this.value.spec, 'rkeConfig.machineGlobalConfig', {});
}
if ( !this.value.spec.rkeConfig.machineSelectorConfig?.length ) {
set(this.value.spec, 'rkeConfig.machineSelectorConfig', [{ config: {} }]);
}
return {
lastIdx: 0,
allPSPs: null,
nodeComponent: null,
credentialId: null,
credential: null,
machinePools: null,
rke2Versions: null,
k3sVersions: null,
s3Backup: false,
chartVersionInfo: null,
versionInfo: {},
membershipUpdate: {},
hasOwner: false,
};
},
computed: {
...mapGetters({ allCharts: 'catalog/charts' }),
...mapGetters({ features: 'features/get' }),
rkeConfig() {
return this.value.spec.rkeConfig;
},
chartValues() {
return this.value.spec.rkeConfig.chartValues;
},
serverConfig() {
return this.value.spec.rkeConfig.machineGlobalConfig;
},
agentConfig() {
// The one we want is the first one with no selector.
// If there are multiple with no selector, that will fall under the unsupported message below.
return this.value.spec.rkeConfig.machineSelectorConfig.find(x => !x.machineLabelSelector).config;
},
// kubeletConfigs() {
// return this.value.spec.rkeConfig.machineSelectorConfig.filter(x => !!x.machineLabelSelector);
// },
unsupportedSelectorConfig() {
let global = 0;
let kubeletOnly = 0;
let other = 0;
// The form supports one config that has no selector for all the main parts
// And one or more configs that have a selector and exactly only kubelet-args.
// If there are any other properties set, or multiple configs with no selector
// show a warning that you're editing only part of the config in the UI.
for ( const conf of this.value.spec?.rkeConfig?.machineSelectorConfig ) {
if ( conf.machineLabelSelector ) {
const keys = Object.keys(conf.config || {});
if ( keys.length === 0 || (keys.length === 1 && keys[0] === 'kubelet-arg') ) {
kubeletOnly++;
} else {
other++;
}
} else {
global++;
}
}
// eslint-disable-next-line no-console
console.log(`Global: ${ global }, Kubelet Only: ${ kubeletOnly }, Other: ${ other }`);
return ( global > 1 || other > 0 );
},
versionOptions() {
function filterAndMap(versions, minVersion) {
const out = (versions || []).filter(obj => !!obj.serverArgs).map((obj) => {
let disabled = false;
if ( minVersion ) {
disabled = compare(obj.id, minVersion) < 0;
}
return {
label: obj.id,
value: obj.id,
sort: sortable(obj.id),
serverArgs: obj.serverArgs,
agentArgs: obj.agentArgs,
charts: obj.charts,
disabled,
};
});
return sortBy(out, 'sort:desc');
}
const cur = this.originalValue?.spec?.kubernetesVersion || '';
const existingRke2 = this.mode === _EDIT && cur.includes('rke2');
const existingK3s = this.mode === _EDIT && cur.includes('k3s');
const rke2 = filterAndMap(this.rke2Versions, (existingRke2 ? cur : null));
const k3s = filterAndMap(this.k3sVersions, (existingK3s ? cur : null));
const showRke2 = rke2.length && !existingK3s;
const showK3s = k3s.length && !existingRke2;
const out = [];
if ( showRke2 ) {
if ( showK3s ) {
out.push({ kind: 'group', label: this.t('cluster.provider.rke2') });
}
out.push(...rke2);
}
if ( showK3s ) {
if ( showRke2 ) {
out.push({ kind: 'group', label: this.t('cluster.provider.k3s') });
}
out.push(...k3s);
}
if ( cur ) {
const existing = out.find(x => x.value === cur);
if ( existing ) {
existing.disabled = false;
} else {
out.unshift({ label: `${ cur } (current)`, value: cur });
}
}
return out;
},
isK3s() {
return (this.value?.spec?.kubernetesVersion || '').includes('k3s');
},
profileOptions() {
const out = (this.agentArgs.profile?.options || []).map((x) => {
return { label: x, value: x };
});
out.unshift({ label: '(None)', value: '' });
return out;
},
pspOptions() {
if ( this.isK3s ) {
return null;
}
const out = [{ label: 'RKE2 Default', value: '' }];
if ( this.allPSPs ) {
for ( const pspt of this.allPSPs ) {
out.push({
label: pspt.nameDisplay,
value: pspt.id,
});
}
}
const cur = this.value.spec.defaultPodSecurityPolicyTemplateName;
if ( cur && !out.find(x => x.value === cur) ) {
out.unshift({ label: `${ cur } (Current)`, value: cur });
}
return out;
},
disableOptions() {
return this.serverArgs.disable.options.map((value) => {
return {
label: this.$store.getters['i18n/withFallback'](`cluster.${ this.isK3s ? 'k3s' : 'rke2' }.systemService."${ value }"`, null, value.replace(/^(rke2|rancher)-/, '')),
value,
};
});
},
cloudProviderOptions() {
const out = [{ label: '(None)', value: '' }];
const preferred = this.$store.getters['plugins/cloudProviderForDriver'](this.provider);
for ( const opt of this.agentArgs['cloud-provider-name'].options ) {
if ( !preferred || opt === preferred || opt === 'external' ) {
out.push({
label: this.$store.getters['i18n/withFallback'](`cluster.cloudProvider."${ opt }".label`, null, opt),
value: opt,
});
}
}
const cur = this.agentConfig['cloud-provider-name'];
if ( cur && !out.find(x => x.value === cur) ) {
out.unshift({ label: `${ cur } (Current)`, value: cur });
}
return out;
},
selectedVersion() {
const str = this.value.spec.kubernetesVersion;
if ( !str ) {
return;
}
const out = findBy(this.versionOptions, 'value', str);
return out;
},
haveArgInfo() {
if ( this.selectedVersion?.serverArgs && this.selectedVersion?.agentArgs ) {
return true;
}
return false;
},
serverArgs() {
return this.selectedVersion?.serverArgs || {};
},
agentArgs() {
return this.selectedVersion?.agentArgs || {};
},
chartVersions() {
return this.selectedVersion?.charts || {};
},
showCisProfile() {
return this.provider === 'custom' && ( this.serverArgs.profile || this.agentArgs.profile );
},
needCredential() {
if ( this.provider === 'custom' || this.provider === 'import' ) {
return false;
}
return true;
},
hasMachinePools() {
if ( this.provider === 'custom' || this.provider === 'import' ) {
return false;
}
return true;
},
machineConfigSchema() {
if ( !this.hasMachinePools ) {
return null;
}
const schema = this.$store.getters['management/schemaFor'](`${ CAPI.MACHINE_CONFIG_GROUP }.${ this.provider }config`);
return schema;
},
nodeTotals() {
const roles = ['etcd', 'controlPlane', 'worker'];
const counts = {};
const out = {
color: {},
label: {},
icon: {},
tooltip: {},
};
for ( const role of roles ) {
counts[role] = 0;
out.color[role] = 'bg-success';
out.icon[role] = 'icon-checkmark';
}
for ( const row of this.machinePools ) {
if ( row.remove ) {
continue;
}
const qty = parseInt(row.pool.quantity, 10);
if ( isNaN(qty) ) {
continue;
}
for ( const role of roles ) {
counts[role] = counts[role] + (row.pool[`${ role }Role`] ? qty : 0);
}
}
for ( const role of roles ) {
out.label[role] = this.t(`cluster.machinePool.nodeTotals.label.${ role }`, { count: counts[role] });
out.tooltip[role] = this.t(`cluster.machinePool.nodeTotals.tooltip.${ role }`, { count: counts[role] });
}
if ( counts.etcd === 0 ) {
out.color.etcd = 'bg-error';
out.icon.etcd = 'icon-x';
} else if ( counts.etcd === 1 || counts.etcd % 2 === 0 || counts.etcd > 7 ) {
out.color.etcd = 'bg-warning';
out.icon.etcd = 'icon-warning';
}
if ( counts.controlPlane === 0 ) {
out.color.controlPlane = 'bg-error';
out.icon.controlPlane = 'icon-x';
} else if ( counts.controlPlane === 1 ) {
out.color.controlPlane = 'bg-warning';
out.icon.controlPlane = 'icon-warning';
}
if ( counts.worker === 0 ) {
out.color.worker = 'bg-error';
out.icon.worker = 'icon-x';
} else if ( counts.worker === 1 ) {
out.color.worker = 'bg-warning';
out.icon.worker = 'icon-warning';
}
return out;
},
enabledSystemServices: {
get() {
const out = difference(this.serverArgs.disable.options, this.serverConfig.disable || []);
return out;
},
set(neu) {
const out = difference(this.serverArgs.disable.options, neu);
set(this.serverConfig, 'disable', out);
},
},
showCloudConfigYaml() {
if ( !this.agentArgs['cloud-provider-name'] ) {
return false;
}
const name = this.agentConfig['cloud-provider-name'];
if ( !name ) {
return false;
}
switch ( name ) {
case 'none': return false;
case 'aws': return false;
case 'rancher-vsphere': return false;
default: return true;
}
},
showVsphereNote() {
if ( !this.agentArgs['cloud-provider-name'] ) {
return false;
}
const name = this.agentConfig['cloud-provider-name'];
return name === 'rancher-vsphere';
},
showCni() {
return !!this.serverArgs.cni;
},
addonNames() {
const names = [];
const cni = this.serverConfig.cni;
if ( cni ) {
const parts = cni.split('+').map(x => `rke2-${ x }`);
names.push(...parts);
}
if ( this.agentConfig['cloud-provider-name'] === 'rancher-vsphere' ) {
names.push('rancher-vsphere-cpi', 'rancher-vsphere-csi');
}
return names;
},
addonVersions() {
const versions = this.addonNames.map(name => this.chartVersionFor(name));
return versions.filter(x => !!x);
},
showk8s21LegacyWarning() {
const isLegacyEnabled = this.features(LEGACY);
if (!isLegacyEnabled) {
return false;
}
const selectedVersion = semver.coerce(this.value.spec.kubernetesVersion);
return semver.satisfies(selectedVersion, '>=1.21.0');
}
},
watch: {
s3Backup(neu) {
if ( neu ) {
set(this.rkeConfig.etcd, 's3', {});
} else {
set(this.rkeConfig.etcd, 's3', null);
}
},
credentialId(val) {
if ( val ) {
this.credential = this.$store.getters['rancher/byId'](NORMAN.CLOUD_CREDENTIAL, this.credentialId);
if ( this.credential ) {
this.value.spec.cloudCredentialSecretName = this.credential.id;
} else {
this.value.spec.cloudCredentialSecretName = null;
}
}
},
addonNames(neu, old) {
const diff = difference(neu, old);
if (!this.$fetchState.pending && diff.length ) {
this.$fetch();
}
},
hasOwner() {
if (this.hasOwner) {
this.$set(this, 'errors', this.errors.filter(e => e !== this.t('cluster.haveOneOwner')));
} else {
this.errors.push(this.t('cluster.haveOneOwner'));
}
}
},
mounted() {
window.rke = this;
},
created() {
this.registerBeforeHook(this.saveMachinePools, 'save-machine-pools');
this.registerAfterHook(this.cleanupMachinePools, 'cleanup-machine-pools');
this.registerAfterHook(this.saveRoleBindings, 'save-role-bindings');
},
methods: {
nlToBr,
set,
async initMachinePools(existing) {
const out = [];
if ( existing?.length ) {
for ( const pool of existing ) {
const config = await this.$store.dispatch('management/find', {
type: `${ CAPI.MACHINE_CONFIG_GROUP }.${ pool.machineConfigRef.kind.toLowerCase() }`,
id: `${ this.value.metadata.namespace }/${ pool.machineConfigRef.name }`,
});
// @TODO what if the pool is missing?
const id = `pool${ ++this.lastIdx }`;
out.push({
id,
remove: false,
create: false,
update: true,
pool: clone(pool),
config: await this.$store.dispatch('management/clone', { resource: config }),
});
}
}
this.machinePools = out;
},
async addMachinePool(idx) {
if ( !this.machineConfigSchema ) {
return;
}
const numCurrentPools = this.machinePools.length || 0;
const config = await this.$store.dispatch('management/createPopulated', {
type: this.machineConfigSchema.id,
metadata: { namespace: DEFAULT_WORKSPACE }
});
config.applyDefaults(idx, this.machinePools);
const name = `pool${ ++this.lastIdx }`;
this.machinePools.push({
id: name,
config,
remove: false,
create: true,
update: false,
pool: {
name,
etcdRole: numCurrentPools === 0,
controlPlaneRole: numCurrentPools === 0,
workerRole: true,
hostnamePrefix: '',
labels: {},
quantity: 1,
machineConfigRef: {
kind: this.machineConfigSchema.attributes.kind,
name: null,
},
},
});
this.$nextTick(() => {
if ( this.$refs.pools?.select ) {
this.$refs.pools.select(name);
}
});
},
removeMachinePool(idx) {
const entry = this.machinePools[idx];
if ( !entry ) {
return;
}
if ( entry.create ) {
// If this is a new pool that isn't saved yet, it can just be dropped
removeObject(this.machinePools, entry);
} else {
// Mark for removal on save
entry.remove = true;
}
},
async saveMachinePools() {
const finalPools = [];
for ( const entry of this.machinePools ) {
// Capitals and such aren't allowed;
set(entry.pool, 'name', normalizeName(entry.pool.name) || 'pool');
const prefix = `${ this.value.metadata.name }-${ entry.pool.name }`.substr(0, 50).toLowerCase();
if ( entry.create ) {
if ( !entry.config.metadata?.name ) {
entry.config.metadata.generateName = `nc-${ prefix }-`;
}
const neu = await entry.config.save();
entry.config = neu;
entry.pool.machineConfigRef.name = neu.metadata.name;
entry.create = false;
entry.update = true;
}
if ( !entry.pool.hostnamePrefix ) {
entry.pool.hostnamePrefix = `${ prefix }-`;
}
finalPools.push(entry.pool);
}
this.value.spec.rkeConfig.machinePools = finalPools;
},
async cleanupMachinePools() {
for ( const entry of this.machinePools ) {
if ( entry.remove ) {
await entry.config.remove();
entry.remove = false;
}
}
},
async saveRoleBindings() {
await this.value.waitForMgmt();
if (this.membershipUpdate.save) {
await this.membershipUpdate.save(this.value.mgmt.id);
}
},
validationPassed() {
return (this.provider === 'custom' || !!this.credentialId) && this.hasOwner;
},
cancelCredential() {
if ( this.$refs.cruresource ) {
this.$refs.cruresource.emitOrRoute();
}
},
done() {
let routeName = 'c-cluster-product-resource';
if ( this.mode === _CREATE && (this.provider === 'import' || this.provider === 'custom') ) {
// Go show the registration command
routeName = 'c-cluster-product-resource-namespace-id';
}
this.$router.push({
name: routeName,
params: {
cluster: this.$route.params.cluster,
product: this.$store.getters['productId'],
resource: CAPI.RANCHER_CLUSTER,
namespace: this.value.metadata.namespace,
id: this.value.metadata.name,
},
});
},
async saveOverride(btnCb) {
if ( this.errors ) {
clear(this.errors);
}
for (const [index] of this.machinePools.entries()) { // validator machine config
if ( typeof this.$refs.pool[index]?.test === 'function' ) {
try {
const res = await this.$refs.pool[index].test();
if (Array.isArray(res) && res.length > 0) {
this.errors.push(...res);
}
} catch (e) {
this.errors.push(e);
}
}
}
if (this.errors.length) {
btnCb(false);
return;
}
await this.save(btnCb);
},
cancel() {
this.$router.push({
name: 'c-cluster-product-resource',
params: {
cluster: this.$route.params.cluster,
product: this.$store.getters['productId'],
resource: CAPI.RANCHER_CLUSTER,
},
});
},
chartVersionFor(chartName) {
const entry = this.chartVersions[chartName];
if ( !entry ) {
return null;
}
const out = this.$store.getters['catalog/version']({
repoType: 'cluster',
repoName: entry.repo,
chartName,
versionName: entry.version,
});
return out;
},
labelForAddon(name) {
const fallback = `${ camelToTitle(name.replace(/^(rke|rke2|rancher)-/, '')) } Configuration`;
return this.$store.getters['i18n/withFallback'](`cluster.addonChart."${ name }"`, null, fallback);
},
refreshYamls() {
const keys = Object.keys(this.$refs).filter(x => x.startsWith('yaml'));
for ( const k of keys ) {
const entry = this.$refs[k];
const list = isArray(entry) ? entry : [entry];
for ( const component of list ) {
component.refresh();
}
}
},
updateValues(name, values) {
set(this.chartValues, name, values);
this.syncChartValues();
},
syncChartValues: throttle(function() {
const out = {};
for ( const k of this.addonNames ) {
const fromChart = this.versionInfo[k].values;
const fromUser = this.chartValues[k];
const different = diff(fromChart, fromUser);
if ( !isEmpty(different) ) {
out[k] = different;
}
}
set(this.value.spec.rkeConfig, 'chartValues', out);
}, 250, { leading: true }),
onMembershipUpdate(update) {
this.$set(this, 'membershipUpdate', update);
},
onHasOwnerChanged(hasOwner) {
this.$set(this, 'hasOwner', hasOwner);
},
canRemoveKubeletRow(row, idx) {
return idx !== 0;
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<Banner v-else-if="$fetchState.error" color="error" :label="$fetchState.error" />
<CruResource
v-else
ref="cruresource"
:mode="mode"
:validation-passed="validationPassed()"
:resource="value"
:errors="errors"
:cancel-event="true"
:done-route="doneRoute"
:apply-hooks="applyHooks"
@done="done"
@finish="saveOverride"
@cancel="cancel"
@error="e=>errors = e"
>
<SelectCredential
v-if="needCredential"
v-model="credentialId"
:mode="mode"
:provider="provider"
:cancel="cancelCredential"
/>
<div v-if="credentialId || !needCredential" class="mt-20">
<NameNsDescription
v-if="!isView"
v-model="value"
:mode="mode"
:namespaced="false"
name-label="cluster.name.label"
name-placeholder="cluster.name.placeholder"
description-label="cluster.description.label"
description-placeholder="cluster.description.placeholder"
/>
<template v-if="hasMachinePools">
<div class="clearfix">
<h2 v-t="'cluster.tabs.machinePools'" class="pull-left" />
<div v-if="!isView" class="pull-right">
<BadgeState
v-tooltip="nodeTotals.tooltip.etcd"
:color="nodeTotals.color.etcd"
:icon="nodeTotals.icon.etcd"
:label="nodeTotals.label.etcd"
class="mr-10"
/>
<BadgeState
v-tooltip="nodeTotals.tooltip.controlPlane"
:color="nodeTotals.color.controlPlane"
:icon="nodeTotals.icon.controlPlane"
:label="nodeTotals.label.controlPlane"
class="mr-10"
/>
<BadgeState
v-tooltip="nodeTotals.tooltip.worker"
:color="nodeTotals.color.worker"
:icon="nodeTotals.icon.worker"
:label="nodeTotals.label.worker"
/>
</div>
</div>
<Tabbed
ref="pools"
:side-tabs="true"
:show-tabs-add-remove="!isView"
@addTab="addMachinePool($event)"
@removeTab="removeMachinePool($event)"
>
<Tab v-for="obj in machinePools" :key="obj.id" :name="obj.id" :label="obj.pool.name || '(Not Named)'" :show-header="false">
<MachinePool
ref="pool"
:value="obj"
:mode="mode"
:provider="provider"
:credential-id="credentialId"
:show-warning="obj.update"
@error="e=>errors = e"
/>
</Tab>
</Tabbed>
<div class="spacer" />
</template>
<h2 v-t="'cluster.tabs.cluster'" />
<Tabbed :side-tabs="true">
<Tab name="basic" label-key="cluster.tabs.basic" :weight="11" @active="refreshYamls">
<Banner v-if="!haveArgInfo" color="warning" label="Configuration information is not available for the selected Kubernetes version. The options available in this screen will be limited, you may want to use the YAML editor." />
<Banner v-if="showk8s21LegacyWarning" color="warning" :label="t('cluster.legacyWarning')" />
<div class="row">
<div class="col" :class="{'span-4': showCni, 'span-6': !showCni}">
<LabeledSelect
v-model="value.spec.kubernetesVersion"
:mode="mode"
:options="versionOptions"
label-key="cluster.kubernetesVersion.label"
/>
</div>
<div v-if="showCni" class="col span-4">
<LabeledSelect
v-model="serverConfig.cni"
:mode="mode"
:options="serverArgs.cni.options"
label="Container Network Provider"
/>
</div>
<div v-if="agentArgs['cloud-provider-name']" class="col" :class="{'span-4': showCni, 'span-6': !showCni}">
<LabeledSelect
v-model="agentConfig['cloud-provider-name']"
:mode="mode"
:options="cloudProviderOptions"
label="Cloud Provider"
/>
</div>
</div>
<template v-if="showVsphereNote">
<Banner color="warning" label-key="cluster.cloudProvider.rancher-vsphere.note" />
</template>
<template v-else-if="showCloudConfigYaml">
<div class="spacer" />
<div class="col span-12">
<h3>Cloud Provider Config</h3>
<YamlEditor
ref="yaml"
v-model="agentConfig['cloud-provider-config']"
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
initial-yaml-values="# Cloud Provider Config"
class="yaml-editor"
/>
</div>
</template>
<div class="spacer" />
<h3>Security</h3>
<div class="row">
<div class="col span-6">
<LabeledSelect
v-if="pspOptions"
v-model="value.spec.defaultPodSecurityPolicyTemplateName"
:mode="mode"
:options="pspOptions"
label="Default Pod Security Policy"
/>
</div>
<div v-if="showCisProfile" class="col span-6">
<LabeledSelect
v-if="serverArgs.profile"
v-model="serverConfig.profile"
:mode="mode"
:options="profileOptions"
label="Server CIS Profile"
/>
<LabeledSelect
v-else-if="agentArgs.profile"
v-model="agentConfig.profile"
:mode="mode"
:options="profileOptions"
label="Worker CIS Profile"
/>
</div>
</div>
<div class="row">
<div class="col span-12 mt-20">
<Checkbox v-if="serverArgs['secrets-encryption']" v-model="serverConfig['secrets-encryption']" :mode="mode" label="Encrypt Secrets" />
<Checkbox v-model="value.spec.enableNetworkPolicy" :mode="mode" label="Project Network Isolation" />
<Checkbox v-if="agentArgs.selinux" v-model="agentConfig.selinux" :mode="mode" label="SELinux" />
</div>
</div>
<div class="spacer" />
<div v-if="serverArgs.disable" class="row">
<div class="col span-12">
<div><h3>System Services</h3></div>
<Checkbox
v-for="opt in disableOptions"
:key="opt.value"
v-model="enabledSystemServices"
:mode="mode"
:label="opt.label"
:value-when-true="opt.value"
/>
</div>
</div>
</Tab>
<Tab name="memberRoles" label-key="cluster.tabs.memberRoles" :weight="10">
<Banner v-if="isEdit" color="info">
{{ t('cluster.memberRoles.removeMessage') }}
</Banner>
<ClusterMembershipEditor :mode="mode" :parent-id="value.mgmt ? value.mgmt.id : null" @membership-update="onMembershipUpdate" @has-owner-changed="onHasOwnerChanged" />
</Tab>
<Tab name="etcd" label-key="cluster.tabs.etcd">
<div class="row">
<div class="col span-6">
<RadioGroup
v-model="rkeConfig.etcd.disableSnapshots"
name="etcd-disable-snapshots"
:options="[true, false]"
label="Automatic Snapshots"
:labels="['Disable','Enable']"
:mode="mode"
/>
</div>
</div>
<div v-if="rkeConfig.etcd.disableSnapshots !== true" class="row">
<div class="col span-6">
<LabeledInput v-model="rkeConfig.etcd.snapshotScheduleCron" :mode="mode" label="Cron Schedule" />
</div>
<div class="col span-6">
<UnitInput
v-model="rkeConfig.etcd.snapshotRetention"
:mode="mode"
label="Keep the last"
suffix="Snapshots"
/>
</div>
</div>
<template v-if="rkeConfig.etcd.disableSnapshots !== true">
<div class="spacer" />
<RadioGroup
v-model="s3Backup"
name="etcd-s3"
:options="[false, true]"
label="Backup Snapshots to S3"
:labels="['Disable','Enable']"
:mode="mode"
/>
<S3Config
v-if="rkeConfig.etcd.s3"
v-model="rkeConfig.etcd.s3"
:namespace="value.metadata.namespace"
:register-before-hook="registerBeforeHook"
:mode="mode"
/>
</template>
<div class="spacer" />
<div class="row">
<div class="col span-6">
<RadioGroup
v-if="serverArgs['etcd-expose-metrics']"
v-model="serverConfig['etcd-expose-metrics']"
name="etcd-expose-metrics"
:options="[false, true]"
label="Metrics"
:labels="['Only available inside the cluster','Exposed to the public interface']"
:mode="mode"
/>
</div>
</div>
</Tab>
<Tab v-if="haveArgInfo" name="networking" label-key="cluster.tabs.networking">
<h3>Addressing</h3>
<div class="row mb-20">
<div v-if="serverArgs['cluster-cidr']" class="col span-6">
<LabeledInput v-model="serverConfig['cluster-cidr']" :mode="mode" label="Cluster CIDR" />
</div>
<div v-if="serverArgs['service-cidr']" class="col span-6">
<LabeledInput v-model="serverConfig['service-cidr']" :mode="mode" label="Service CIDR" />
</div>
</div>
<div class="row mb-20">
<div v-if="serverArgs['cluster-dns']" class="col span-6">
<LabeledInput v-model="serverConfig['cluster-dns']" :mode="mode" label="Cluster DNS" />
</div>
<div v-if="serverArgs['cluster-domain']" class="col span-6">
<LabeledInput v-model="serverConfig['cluster-domain']" :mode="mode" label="Cluster Domain" />
</div>
</div>
<div v-if="serverArgs['service-node-port-range']" class="row mb-20">
<div class="col span-6">
<LabeledInput v-model="serverConfig['service-node-port-range']" :mode="mode" label="NodePort Service Port Range" />
</div>
</div>
<div v-if="serverArgs['tls-san']" class="row mb-20">
<div class="col span-6">
<ArrayList v-model="serverConfig['tls-san']" :protip="false" :mode="mode" title="TLS Alternate Names" />
</div>
</div>
<ACE v-model="value" :mode="mode" />
</Tab>
<Tab name="upgrade" label-key="cluster.tabs.upgrade">
<div class="row">
<div class="col span-6">
<h3>Control Plane</h3>
<LabeledInput v-model="rkeConfig.upgradeStrategy.controlPlaneConcurrency" :mode="mode" label="Control Plane Concurrency" tooltip="This can be either a fixed number of nodes (e.g. 1) at a time of a percentage (e.g. 10%)" />
<div class="spacer" />
<DrainOptions v-model="rkeConfig.upgradeStrategy.controlPlaneDrainOptions" :mode="mode" />
</div>
<div class="col span-6">
<h3>Worker Nodes</h3>
<LabeledInput v-model="rkeConfig.upgradeStrategy.workerConcurrency" :mode="mode" label="Worker Concurrency" tooltip="This can be either a fixed number of nodes (e.g. 1) at a time of a percentage (e.g. 10%)" />
<div class="spacer" />
<DrainOptions v-model="rkeConfig.upgradeStrategy.workerDrainOptions" :mode="mode" />
</div>
</div>
</Tab>
<Tab name="registry" label-key="cluster.tabs.registry">
<RegistryMirrors
v-model="value"
:mode="mode"
/>
<RegistryConfigs
v-model="value"
class="mt-20"
:mode="mode"
:register-before-hook="registerBeforeHook"
/>
</Tab>
<Tab name="addons" label-key="cluster.tabs.addons" @active="refreshYamls">
<div v-if="addonVersions.length">
<div v-for="v in addonVersions" :key="v._key">
<h3>{{ labelForAddon(v.name) }}</h3>
<Questions
v-if="versionInfo[v.name] && versionInfo[v.name].questions"
v-model="chartValues[v.name]"
:mode="mode"
:tabbed="false"
:source="versionInfo[v.name]"
:target-namespace="value.metadata.namespace"
/>
<YamlEditor
v-else
ref="yaml-values"
:value="chartValues[v.name]"
:scrolling="true"
:initial-yaml-values="versionInfo[v.name] ? versionInfo[v.name].values : ''"
:as-object="true"
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
:hide-preview-buttons="true"
@input="data => updateValues(v.name, data)"
/>
<div class="spacer" />
</div>
</div>
<div>
<h3>
Additional Manifest
<i v-tooltip="'Additional Kubernetes Manifet YAML to be applied to the cluster on startup.'" class="icon icon-info" />
</h3>
<YamlEditor
ref="yaml-additional"
v-model="rkeConfig.additionalManifest"
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
initial-yaml-values="# Additional Manifest YAML"
class="yaml-editor"
/>
</div>
</Tab>
<Tab name="advanced" label-key="cluster.tabs.advanced" :weight="-1" @active="refreshYamls">
<template v-if="haveArgInfo">
<h3>Additional Kubelet Args</h3>
<ArrayListGrouped
v-if="agentArgs['kubelet-arg']"
v-model="rkeConfig.machineSelectorConfig"
class="mb-20"
add-label="Add Machine Selector"
:can-remove="canRemoveKubeletRow"
:default-add-value="{machineLabelSelector: { matchExpressions: [], matchLabels: {} }, config: {'kubelet-arg': []}}"
>
<template #default="{row}">
<template v-if="row.value.machineLabelSelector">
<h3>For machines with labels matching:</h3>
<MatchExpressions
v-model="row.value.machineLabelSelector"
class="mb-20"
:mode="mode"
:show-remove="false"
:initial-empty-row="true"
/>
<h3>Use the Kubelet args:</h3>
</template>
<h3 v-else>
For <span v-if="rkeConfig.machineSelectorConfig.length > 1">any</span><span v-else>all</span> machines, use the Kubelet args:
</h3>
<ArrayList
v-model="row.value.config['kubelet-arg']"
:mode="mode"
add-label="Add Argument"
:initial-empty-row="!!row.value.machineLabelSelector"
/>
</template>
</ArrayListGrouped>
<Banner v-if="rkeConfig.machineSelectorConfig.length > 1" color="info" label="Note: The last selector that matches wins and only args from it will be used. Args from other matches above will not combined together or merged." />
<ArrayList v-if="serverArgs['kube-controller-manager-arg']" v-model="serverConfig['kube-controller-manager-arg']" :mode="mode" title="Additional Controller Manager Args" class="mb-20" />
<ArrayList v-if="serverArgs['kube-apiserver-arg']" v-model="serverConfig['kube-apiserver-arg']" :mode="mode" title="Additional API Server Args" class="mb-20" />
<ArrayList v-if="serverArgs['kube-scheduler-arg']" v-model="serverConfig['kube-scheduler-arg']" :mode="mode" title="Additional Scheduler Args" />
</template>
<template v-if="agentArgs['protect-kernel-defaults']">
<div class="spacer" />
<div class="row">
<div class="col span-12">
<Checkbox v-model="agentConfig['protect-kernel-defaults']" :mode="mode" label="Raise error if kernel parameters are different than the expected kubelet defaults" />
</div>
</div>
</template>
</Tab>
<AgentEnv v-model="value" :mode="mode" />
<Labels v-model="value" :mode="mode" />
</Tabbed>
</div>
<Banner v-if="unsupportedSelectorConfig" color="warning" label="This cluster contains a machineSelectorConfig which this form does not fully support; use the YAML editor to manage the full configuration." />
<template v-if="needCredential && !credentialId" #form-footer>
<div><!-- Hide the outer footer --></div>
</template>
</CruResource>
</template>