import ClusterDriver from 'shared/mixins/cluster-driver'; import { equal, alias, or } from '@ember/object/computed'; import { get, set, computed, observer, setProperties, defineProperty } from '@ember/object'; import { maxSatisfying } from 'shared/utils/parse-version'; import { inject as service } from '@ember/service'; import { removeEmpty, keysToCamel, validateEndpoint, keysToDecamelize } from 'shared/utils/util'; import { validateHostname } from '@rancher/ember-api-store/utils/validate'; import { validateCertWeakly } from 'shared/utils/util'; import C from 'shared/utils/constants'; import jsyaml from 'js-yaml'; import layout from './template'; import { resolve } from 'rsvp'; import { isEmpty } from '@ember/utils'; import InputTextFile from 'ui/components/input-text-file/component'; import { scheduleOnce, once, next } from '@ember/runloop'; import { azure as AzureInfo } from 'shared/components/cru-cloud-provider/cloud-provider-info'; import moment from 'moment'; import ManageLabels from 'shared/mixins/manage-labels'; import { typeOf } from '@ember/utils'; import Semver, { major, minor } from 'semver'; import { on } from '@ember/object/evented'; import deepSet from 'ember-deep-set'; import { coerceVersion } from 'shared/utils/parse-version'; const EXCLUDED_KEYS = ['extra_args']; function camelToUnderline(str, split = true) { str = (str || ''); if ( str.indexOf('-') > -1 || str.endsWith('CloudProvider')) { return str; } else if ( split ) { return (str || '').dasherize().split('-').join('_'); } else { return (str || '').dasherize(); } } const EXCLUED_CLUSTER_OPTIONS = [ 'annotations', 'labels' ]; const AUTHCHOICES = [ { label: 'clusterNew.rke.auth.x509', value: 'x509' }, ]; const INGRESSCHOICES = [ { label: 'clusterNew.rke.ingress.nginx', value: 'nginx' }, { label: 'clusterNew.rke.ingress.none', value: 'none' }, ]; const AVAILABLE_STRATEGIES = ['local', 's3']; const { CLUSTER_TEMPLATE_IGNORED_OVERRIDES, NETWORK_CONFIG_DEFAULTS: { DEFAULT_BACKEND_TYPE } } = C; export default InputTextFile.extend(ManageLabels, ClusterDriver, { globalStore: service(), settings: service(), growl: service(), intl: service(), clusterTemplates: service(), access: service(), router: service(), layout, authChoices: AUTHCHOICES, ingressChoices: INGRESSCHOICES, availableStrategies: AVAILABLE_STRATEGIES, ingornedRkeOverrides: CLUSTER_TEMPLATE_IGNORED_OVERRIDES, configField: 'rancherKubernetesEngineConfig', registry: 'default', accept: '.yml, .yaml', backupStrategy: 'local', overrideCreatLabel: null, loading: false, pasteOrUpload: false, model: null, initialVersion: null, registryUrl: null, registryUser: null, registryPass: null, clusterOptErrors: null, nodeNameErrors: null, existingNodes: null, initialNodeCounts: null, step: 1, token: null, taints: null, labels: null, etcd: false, controlplane: false, worker: true, defaultDockerRootDir: null, nodePoolErrors: null, windowsEnable: false, isLinux: true, weaveCustomPassword: false, clusterTemplateCreate: false, clusterTemplateQuestions: null, forceExpandOnInit: false, forceExpandAll: false, applyClusterTemplate: null, useClusterTemplate: false, clusterTemplateRevisionId: null, clusterTemplatesEnforced: false, selectedClusterTemplateId: null, isNew: equal('mode', 'new'), isEdit: equal('mode', 'edit'), notView: or('isNew', 'isEdit'), clusterState: alias('model.originalCluster.state'), // Custom stuff isCustom: equal('nodeWhich', 'custom'), init() { this._super(...arguments); this.initNodeCounts(); if (!this.useClusterTemplate && this.clusterTemplateRevisionId) { setProperties(this, { useClusterTemplate: true, forceExpandOnInit: true, selectedClusterTemplateId: this.cluster.clusterTemplateId, }) } if (this.applyClusterTemplate) { if (this.useClusterTemplate) { this.initTemplateCluster(); } else { this.initNonTemplateCluster(); } } else { if (this.clusterTemplateRevisionId && this.router.currentRouteName !== 'global-admin.cluster-templates.new') { this.initTemplateCluster(); } else { this.initNonTemplateCluster(); } } }, didReceiveAttrs() { if ( get(this, 'isEdit') && !get(this, 'clusterTemplateCreate')) { this.loadToken(); } if (this.applyClusterTemplate) { if (this.useClusterTemplate) { this.initTemplateCluster(); } else { if (!this.clusterTemplateCreate && isEmpty(this.cluster.rancherKubernetesEngineConfig)) { this.initNonTemplateCluster(); } } } if (this.useClusterTemplate && this.clusterTemplateRevisionId) { set(this, 'forceExpandAll', true); } }, actions: { fileUploaded() { let { yamlValue } = this; next(() => { set(this, 'clusterOptErrors', this.checkYamlForRkeConfig(yamlValue)); }); }, yamlValUpdated(yamlValue, codeMirror) { if (!this.codeMirror) { codeMirror.on('paste', this.pastedYaml.bind(this)); } // this fires when we first change over so here is where you can set the paste watcher to validate setProperties(this, { yamlValue, codeMirror }); }, addOverride(enabled, paramsToOverride, hideQuestion = false) { let { primaryResource, clusterTemplateQuestions = [], } = this; let { path } = paramsToOverride; let question = null; let questionsSchemas = []; let { clusterTemplateRevision } = this.model; if (clusterTemplateRevision && ( this.clusterTemplates.questionsSchemas || []).length > 0) { questionsSchemas = this.clusterTemplates.questionsSchemas; } if (enabled) { if (!clusterTemplateQuestions.findBy('variable', path)) { if (path.startsWith('uiOverride')) { question = questionsSchemas.findBy('variable', path); if (!question) { question = this.globalStore.createRecord({ type: 'question', variable: path, primaryResource, }); } hideQuestion = true; setProperties(question, { type: 'boolean', default: true, hideQuestion }); clusterTemplateQuestions.pushObject(question); } else { question = questionsSchemas.findBy('variable', path); if (question) { if (question.variable === 'rancherKubernetesEngineConfig.kubernetesVersion') { question = this.parseKubernetesVersionSemVer(primaryResource, question); } if (question.variable.includes('azureCloudProvider')) { let cloudProviderMatch = AzureInfo[(question.variable.split('.') || []).lastObject]; if (cloudProviderMatch) { let { required = false, type = 'string' } = cloudProviderMatch; question = this.globalStore.createRecord({ type: 'question', variable: path, }); setProperties(question, { required, type, }); if (required) { set(question, 'forceRequired', true); } } } } else { question = this.globalStore.createRecord({ type: 'question', variable: path, }); set(question, 'type', 'string'); } setProperties(question, { primaryResource, isBuiltIn: true, hideQuestion }); if (path.includes('rancherKubernetesEngineConfig.privateRegistries[0]')) { // need to replace the array target with built in first object so the alias works path = path.replace('[0]', '.firstObject'); } defineProperty(question, 'default', alias(`primaryResource.${ path }`)); clusterTemplateQuestions.pushObject(question); } } } else { if (path === 'uiOverrideBackupStrategy') { question = clusterTemplateQuestions.findBy('variable', 'rancherKubernetesEngineConfig.services.etcd.backupConfig.s3BackupConfig'); clusterTemplateQuestions.removeObject(question); } else { question = clusterTemplateQuestions.findBy('variable', path); clusterTemplateQuestions.removeObject(question); } } set(this, 'clusterTemplateQuestions', clusterTemplateQuestions); }, setCpFields(configName, cloudProviderConfig) { set(this, `config.cloudProvider.${ configName }`, cloudProviderConfig); }, cancel() { const newOpts = this.getOptsFromYaml(); if (newOpts) { if (this.updateFromYaml && this.mode !== 'view') { this.updateFromYaml(newOpts); setProperties(this, { clusterOptErrors: [], pasteOrUpload: false, }); } } else { if (isEmpty(this.clusterOptErrors)) { set(this, 'pasteOrUpload', false); } } }, showPaste() { if (!this.configBeforeYaml) { // need to store off a raw non-referneced version of the og config // if a user switches back to form view and then comes back to yaml to // move something to a lower indent or remove we want to only add whats in // the orignal config and waht is in the yaml. // add the key bloop: true to yaml, sswitch to form, wait no i want bloop nested under something else, comeback and move it, now you have two keys, one at root and dupe at new nest set(this, 'configBeforeYaml', this.primaryResource.clone()); } set(this, 'pasteOrUpload', true); }, addRegistry(pr) { const config = get(this, 'config') if (( config.privateRegistries || [] ).length <= 0) { set(config, 'privateRegistries', [pr]); } else { config.privateRegistries.pushObject(pr); } }, removeRegistry(pr) { get(this, 'config.privateRegistries').removeObject(pr); }, setTaints(taints) { set(this, 'taints', taints); } }, usingClusterTemplate: observer('useClusterTemplate', function() { if (!this.useClusterTemplate && this.clusterTemplateRevisionId) { set(this, 'clusterTemplateRevisionId', null); } }), driverDidChange: observer('nodeWhich', function() { this.createRkeConfigWithDefaults(); }), isLinuxChanged: observer('isLinux', function() { if (get(this, 'nodeWhich') !== 'custom') { return } const isLinux = get(this, 'isLinux') if (!isLinux) { setProperties(this, { controlplane: false, etcd: false, worker: true, }) } }), strategyChanged: observer('backupStrategy', function() { const { backupStrategy, globalStore } = this; const services = this.config.services.clone(); switch (backupStrategy) { case 'local': if (services) { setProperties(services.etcd, { backupConfig: globalStore.createRecord({ type: 'backupConfig', s3BackupConfig: null, enabled: get(services, 'etcd.backupConfig.enabled') || true, }) }); } break; case 's3': if (services) { setProperties(services.etcd, { backupConfig: globalStore.createRecord({ type: 'backupConfig', s3BackupConfig: globalStore.createRecord({ type: 's3BackupConfig' }), enabled: get(services, 'etcd.backupConfig.enabled') || true, }) }); } break; default: break; } set(this, 'config.services', services); }), enforcementChanged: on('init', observer('settings.clusterTemplateEnforcement', function() { let { access: { me: { hasAdmin: globalAdmin = null } }, settings: { clusterTemplateEnforcement = false } } = this; let useClusterTemplate = false; if (!globalAdmin) { // setting is string value if (clusterTemplateEnforcement === 'true') { clusterTemplateEnforcement = true; } else { clusterTemplateEnforcement = false; } if (this.applyClusterTemplate) { if (clusterTemplateEnforcement) { useClusterTemplate = true; } else if (this.clusterTemplateRevisionId) { useClusterTemplate = true; } } else if (!this.applyClusterTemplate && clusterTemplateEnforcement) { useClusterTemplate = true; } else { if (this.clusterTemplateRevisionId) { useClusterTemplate = true; } } setProperties(this, { useClusterTemplate, clusterTemplatesEnforced: clusterTemplateEnforcement }); } })), filteredClusterTemplates: computed('model.clusterTemplates.@each.{id,state,name,members}', function() { let { model: { clusterTemplates } } = this; let mapped = clusterTemplates.map((clusterTemplate) => { return { name: clusterTemplate.name, id: clusterTemplate.id, } }); return mapped.sortBy('created').reverse(); }), filteredTemplateRevisions: computed('selectedClusterTemplateId', 'model.clusterTemplateRevisions.@each.{id,state,name,members}', function() { let { selectedClusterTemplateId, clusterTemplateRevisionId = null, model: { clusterTemplateRevisions, clusterTemplates, }, originalCluster, intl } = this; let clusterTemplate; clusterTemplateRevisions = clusterTemplateRevisions.slice().filterBy('enabled').filterBy('clusterTemplateId', selectedClusterTemplateId); clusterTemplate = clusterTemplates.findBy('id', selectedClusterTemplateId) let mapped = clusterTemplateRevisions.map((clusterTemplateRevision) => { let d = moment(clusterTemplateRevision.created); const isDefault = clusterTemplate.defaultRevisionId === clusterTemplateRevision.id; let out = { id: clusterTemplateRevision.id, name: intl.t(`clusterNew.rke.clustersSelectTemplateRevision.select.${ isDefault ? 'default' : 'other' }`, { name: clusterTemplateRevision.name, ago: d.fromNow() }), }; // editing a cluster not a template if (this.isEdit && this.cluster.type === 'cluster') { let kubeVersion = coerceVersion(get(originalCluster, 'rancherKubernetesEngineConfig.kubernetesVersion')); // filter revisions with kube version lower if (Semver.lt(coerceVersion(clusterTemplateRevision.clusterConfig.rancherKubernetesEngineConfig.kubernetesVersion), kubeVersion)) { set(out, 'disabled', true); } } return out; }) if (clusterTemplate && clusterTemplateRevisionId === null ) { once(() => { set(this, 'clusterTemplateRevisionId', clusterTemplate.defaultRevisionId); }); } return mapped.sortBy('created').reverse(); }), allTemplates: computed('model.clusterTemplates.[]', 'model.clusterTemplateRevisions.[]', function() { const remapped = []; let { clusterTemplates, clusterTemplateRevisions } = this.model; clusterTemplateRevisions = clusterTemplateRevisions.filterBy('enabled'); clusterTemplateRevisions.forEach((rev) => { let match = clusterTemplates.findBy('id', get(rev, 'clusterTemplateId')); if (match) { remapped.pushObject({ clusterTemplateId: get(match, 'id'), clusterTemplateName: get(match, 'displayName'), clusterTemplateRevisionId: get(rev, 'id'), clusterTemplateRevisionName: get(rev, 'name'), }); } }); return remapped; }), canEditForm: computed('clusterOptErrors.[]', function() { return (this.clusterOptErrors || []).length === 0; }), kubeApiPodSecurityPolicy: computed('config.services.kubeApi.podSecurityPolicy', { get() { let pspConfig = get(this, 'config.services.kubeApi'); if (typeof pspConfig === 'undefined') { return false; } return get(pspConfig, 'podSecurityPolicy'); }, set(key, value) { if (typeof get(this, 'config.services') === 'undefined') { set(this, 'config.services', get(this, 'globalStore').createRecord({ type: 'rkeConfigServices', kubeApi: get(this, 'globalStore').createRecord({ type: 'kubeAPIService', podSecurityPolicy: value, }), })); } else { set(this, 'config.services', { kubeApi: { podSecurityPolicy: value } }); } return value; } }), monitoringProvider: computed('config.monitoring', { get() { let monitoringConfig = get(this, 'config.monitoring'); if (typeof monitoringConfig === 'undefined') { return null; } return get(monitoringConfig, 'provider'); }, set(key, value) { if (typeof get(this, 'config.monitoring') === 'undefined') { set(this, 'config.monitoring', get(this, 'globalStore').createRecord({ type: 'monitoringConfig', provider: value })); } else { set(this, 'config.monitoring', { provider: value }); } return value; } }), nginxIngressProvider: computed('config.ingress', { get() { let ingressConfig = get(this, 'config.ingress'); if (typeof ingressConfig === 'undefined') { return null; } return get(ingressConfig, 'provider'); }, set(key, value) { if (typeof get(this, 'config.ingress') === 'undefined') { set(this, 'config.ingress', get(this, 'globalStore').createRecord({ type: 'ingressConfig', provider: value })); } else { set(this, 'config.ingress', { provider: value }); } return value; } }), versionChoices: computed(`settings.${ C.SETTING.VERSIONS_K8S }`, function() { let out = JSON.parse(get(this, `settings.${ C.SETTING.VERSIONS_K8S }`) || '{}'); out = Object.keys(out); let patchedOut = []; if (this.clusterTemplateCreate) { patchedOut = out.map((version) => { return `${ major(version) }.${ minor(version) }.x`; }); } return [...out, ...patchedOut]; }), isNodeNameValid: computed('nodeName', function() { const nodeName = (get(this, 'nodeName') || '').toLowerCase(); if ( get(nodeName, 'length') === 0 ) { return true; } else { const errors = validateHostname(nodeName, 'Node Name', get(this, 'intl'), { restricted: true }); set(this, 'nodeNameErrors', errors); return errors.length === 0; } }), isAddressValid: computed('address', function() { return get(this, 'address') === undefined || get(this, 'address.length') === 0 || validateEndpoint(get(this, 'address')); }), isInternalAddressValid: computed('internalAddress', function() { return get(this, 'internalAddress') === undefined || get(this, 'internalAddress.length') === 0 || validateEndpoint(get(this, 'internalAddress')); }), newNodeCount: computed('initialNodeCounts', 'primaryResource.id', 'existingNodes.@each.clusterId', function() { let clusterId = get(this, 'primaryResource.id'); let orig = get(this, 'initialNodeCounts')[clusterId] || 0; let cur = get(this, 'existingNodes').filterBy('clusterId', clusterId).length if ( cur < orig ) { orig = cur; set(get(this, 'initialNodeCounts'), clusterId, cur) } return cur - orig; }), command: computed('taints', 'labels', 'token.nodeCommand', 'token.windowsNodeCommand', 'etcd', 'controlplane', 'worker', 'address', 'internalAddress', 'nodeName', 'isLinux', function() { let out = get(this, 'token.nodeCommand'); if ( !out ) { return; } const address = get(this, 'address'); const nodeName = get(this, 'nodeName'); const internalAddress = get(this, 'internalAddress'); const roles = ['etcd', 'controlplane', 'worker']; const labels = get(this, 'labels') || {}; const taints = get(this, 'taints') || []; const windowsSelected = !get(this, 'isLinux') const windowsCmdPostfix = ' | iex}"'; if (windowsSelected) { out = (get(this, 'token.windowsNodeCommand') || '').replace('--isolation hyperv ', '').replace(windowsCmdPostfix, '') } if ( nodeName ) { out += ` --node-name ${ nodeName.toLowerCase() }`; } if (address) { out += ` --address ${ address }`; } if (internalAddress) { out += ` --internal-address ${ internalAddress }`; } for ( let i = 0, k ; i < roles.length ; i++ ) { k = roles[i]; if ( get(this, k) ) { out += ` --${ k }`; } } Object.keys(labels).forEach((key) => { out += ` --label ${ key }=${ labels[key] }`; }); taints.forEach((taint) => { out += ` --taints ${ get(taint, 'key') }=${ get(taint, 'value') }:${ get(taint, 'effect') }`; }); if (windowsSelected) { out += windowsCmdPostfix } return out; }), yamlValue: computed('pasteOrUpload', { get() { // On edit we should get the cluster fields that are updateable, any fields added during the creation would need the cluster fields at the time const intl = get(this, 'intl'); let config = this.isEdit ? this.getSupportedFields(get(this, 'primaryResource'), 'cluster', EXCLUED_CLUSTER_OPTIONS) : this.getClusterFields(this.primaryResource); let cpConfig = get(config, 'rancher_kubernetes_engine_config.cloud_provider'); // debugger; // some kind of recursion bug config = removeEmpty(config, EXCLUDED_KEYS); // get rid of undefined config = JSON.parse(JSON.stringify(config)); if ( cpConfig ) { cpConfig = removeEmpty(cpConfig, EXCLUDED_KEYS); if (cpConfig.azureCloudProvider) { // this is a quick and dirty fix for azure only because it is currently the only cp that works // this whole process will be recieving updates shortly so this is a temp fix // client_id, secret, & subscription_id are all required so ensure they are there on NEW when a user has entered them but switched to yaml or edit // removeEmpty works great except for these fields and adding nested paths doesn't work in removeEmpty Object.assign(config.rancher_kubernetes_engine_config.cloud_provider.azureCloudProvider, { 'aad_client_cert_password': cpConfig.azureCloudProvider.aad_client_cert_password || '', 'aad_client_id': cpConfig.azureCloudProvider.aad_client_id || '', 'aad_client_secret': cpConfig.azureCloudProvider.aad_client_secret || '', 'tenant_id': cpConfig.azureCloudProvider.tenant_id || '', 'subscription_id': cpConfig.azureCloudProvider.subscription_id || '', }); } } let yaml = jsyaml.safeDump(config, { sortKeys: true }); let lines = yaml.split('\n'); let out = ''; lines.forEach((line, idx) => { if ( line.trim() ) { let key = ''; let commentLines = ''; if (idx === 0) { commentLines = intl.t('').split('\n'); key = `rkeConfigComment.clusterConfig` } else { key = `rkeConfigComment.${ line.split(':')[0].trim() }` } if ( intl.exists(key) ) { commentLines = intl.t(key).split('\n'); commentLines.forEach((commentLine) => { out += `# ${ commentLine.slice(1, commentLine.length - 1) }\n`; }); } out += `${ line.trimEnd() }\n`; } }); return out; }, set(key, value) { next(() => { set(this, 'clusterOptErrors', this.checkYamlForRkeConfig(value)); }); return value; } }), allErrors: computed('errors.[]', 'clusterErrors.[]', 'otherErrors.[]', 'clusterOptErrors.[]', function() { let { errors, clusterErrors, clusterOptErrors, otherErrors, } = this; return [...(errors || []), ...(clusterErrors || []), ...(clusterOptErrors || []), ...(otherErrors || [])]; }), getClusterFields(primaryResource) { let pojoPr = JSON.parse(JSON.stringify(removeEmpty(primaryResource, EXCLUDED_KEYS))); let decamelizedObj = {}; decamelizedObj = keysToDecamelize(pojoPr, void (0), ['type', 'azureCloudProvider']); return decamelizedObj; }, checkYamlForRkeConfig(yamlValue) { let decamledYaml = this.parseOptsFromYaml(yamlValue); let errOut = null; if (decamledYaml && isEmpty(decamledYaml.rancherKubernetesEngineConfig)) { errOut = [`Cluster Options Parse Error: Missing Rancher Kubernetes Engine Config`]; } return errOut; }, pastedYaml(cm) { next(() => { set(this, 'clusterOptErrors', this.checkYamlForRkeConfig(cm.doc.getValue())); }); }, parseOptsFromYaml(yamlValue) { let yamlConfig; try { yamlConfig = jsyaml.safeLoad(yamlValue); } catch ( err ) { set(this, 'clusterOptErrors', [`Cluster Options Parse Error: ${ err.snippet } - ${ err.message }`]); return; } return keysToCamel(yamlConfig); }, getOptsFromYaml() { let { yamlValue } = this; let decamledYaml = this.parseOptsFromYaml(yamlValue); if (decamledYaml && isEmpty(decamledYaml.rancherKubernetesEngineConfig)) { set(this, 'clusterOptErrors', [`Cluster Options Parse Error: Missing rancher_kubernetes_engine_config key`]); return; } decamledYaml.type = this.primaryResource.type; return this.globalStore.createRecord(decamledYaml); }, buildClusterAnswersFromConfig(cluster, questions) { let { backupStrategy } = this; let answers = {}; if (questions && questions.length) { questions.forEach((quest) => { let match = get(cluster, quest.variable); if (match) { answers[quest.variable] = match; } else { if (quest.variable.includes('s3BackupConfig') && backupStrategy === 'local') { // we get into this case when a RKE Template creator lets the user override the backup strategy, and they've changed it to local. // if we dont send the answers with nulls the s3backupconfig will be created on the backend from its existence on the RKE Template revision answers[quest.variable] = null; } } }); } return answers; }, willSave() { const { applyClusterTemplate, cluster, configField: field, } = this; let ok = true; this.checkKubernetesVersionSemVer(); if (this.pasteOrUpload && this.mode !== 'view') { const newOpts = this.getOptsFromYaml(); if (newOpts) { if (this.updateFromYaml) { this.updateFromYaml(newOpts); } } } if (get(cluster, 'localClusterAuthEndpoint')) { if (!get(cluster, 'rancherKubernetesEngineConfig') || isEmpty(get(cluster, 'rancherKubernetesEngineConfig'))) { delete cluster.localClusterAuthEndpoint; } } set(this, 'errors', null); ok = this.validate(); if (ok) { if (typeOf(cluster.clearProvidersExcept) === 'function' || applyClusterTemplate && typeOf(this.buildClusterAnswersFromConfig) === 'function') { if (applyClusterTemplate) { let questions = get(this, 'model.clusterTemplateRevision.questions') || []; let answers = []; let errors = null; answers = this.buildClusterAnswersFromConfig(cluster, questions); if (questions.length > 0) { errors = this.checkRequiredQuestionsHaveAnswers(questions, answers); } if (isEmpty(errors)) { set(cluster, 'answers', { values: answers }); this.cluster.clearConfigFieldsForClusterTemplate(); } else { set(this, 'errors', errors); ok = false; } } else { cluster.clearProvidersExcept(field); } } } return ok; }, checkRequiredQuestionsHaveAnswers(questions, answers) { const { intl } = this; const required = questions.filterBy('required', true); const errors = []; if (questions.length > 0 && required.length > 0) { required.forEach((rq) => { if (!answers[rq.variable]) { errors.push(intl.t('validation.required', { key: rq.variable })); } }) } return errors; }, validate() { let { config, intl } = this; this._super(...arguments); let errors = this.errors || []; if (this.clusterTemplateCreate) { const revision = this.model.clusterTemplateRevision; if ( revision ) { errors.pushObjects(revision.validationErrors()); const cloudProvider = get(revision, 'clusterConfig.rancherKubernetesEngineConfig.cloudProvider.name'); const azureProvider = get(revision, 'clusterConfig.rancherKubernetesEngineConfig.cloudProvider.azureCloudProvider') || {}; if ( cloudProvider === 'azure' ) { const azureQuestions = (get(revision, 'questions') || []).map((x) => x.variable.replace(/^rancherKubernetesEngineConfig\.cloudProvider\.azureCloudProvider\./, '')); const requiredFields = Object.keys(AzureInfo).filter((k) => AzureInfo[k].required); requiredFields.forEach((key) => { if ( !get(azureProvider, key) && !azureQuestions.includes(key)) { errors.push(intl.t('validation.requiredOrOverride', { key })); } }); } } } else { if ( !get(this, 'isCustom') ) { errors.pushObjects(get(this, 'nodePoolErrors')); } if ( get(config, 'cloudProvider.name') === 'azure' && !this.applyClusterTemplate ) { Object.keys(AzureInfo).forEach((key) => { if ( get(AzureInfo, `${ key }.required`) && !get(config, `cloudProvider.azureCloudProvider.${ key }`)) { if ( this.isNew || this.isEdit && key !== 'aadClientSecret' ) { errors.push(intl.t('validation.required', { key })); } } }); } } if ( get(config, 'services.kubeApi.podSecurityPolicy') && !get(this, 'primaryResource.defaultPodSecurityPolicyTemplateId') ) { errors.push(intl.t('clusterNew.psp.required')); } if (get(this, 'config.services.etcd.snapshot')) { errors = this.validateEtcdService(errors); } if ( get(this, 'primaryResource.localClusterAuthEndpoint.enabled') ) { errors = this.validateAuthorizedClusterEndpoint(errors); } set(this, 'errors', errors); return get(this, 'errors.length') === 0; }, validateAuthorizedClusterEndpoint(errors) { let { localClusterAuthEndpoint } = get(this, 'primaryResource'); let { caCerts, fqdn } = localClusterAuthEndpoint; if (caCerts) { if (!validateCertWeakly(caCerts) ) { errors.push(this.intl.t('newCertificate.errors.cert.invalidFormat')); } } if (fqdn) { errors = validateHostname(fqdn, 'FQDN', get(this, 'intl'), { restricted: true }, errors); } return errors; }, validateEtcdService(errors) { const etcdService = get(this, 'config.services.etcd') || {}; const { creation, retention } = etcdService; const that = this; function checkDurationIsValid(duration, type) { // exact matching on these inputs // patternList = 12h12m12s || 12h12m || 12m12s || 12h12s || 12h || 12m || 12s let patternList = [/^(\d+)(h)(\d+)(m)(\d+)(s)$/, /^(\d+)(h)(\d+)(m)$/, /^(\d+)(m)(\d+)(s)$/, /^(\d+)(h)(\d+)(s)$/, /^(\d+)(h)$/, /^(\d+)(m)$/, /^(\d+)(s)$/]; let match = patternList.filter( (p) => p.test(duration) ); if (match.length === 0) { durationError(duration, type); } return; } function durationError(entry, type) { return errors.push(get(that, 'intl').t('clusterNew.rke.etcd.error', { type, entry })); } checkDurationIsValid(creation, 'Creation'); checkDurationIsValid(retention, 'Reteintion'); return errors; }, doneSaving(neu) { let { close } = this; if ( get(this, 'isCustom') ) { if ( get(this, 'isEdit') ) { if (close) { close(neu); } } else { this.loadToken(); } } else { if (close) { close(neu); } } return resolve(); }, loadToken() { const cluster = get(this, 'primaryResource'); if (cluster.getOrCreateToken) { setProperties(this, { step: 2, loading: true }); return cluster.getOrCreateToken().then((token) => { if ( this.isDestroyed || this.isDestroying ) { return; } setProperties(this, { token, loading: false }); }).catch((err) => { if ( this.isDestroyed || this.isDestroying ) { return; } get(this, 'growl').fromError('Error getting command', err); set(this, 'loading', false); }); } else { return; } }, findExcludedKeys(resourceFields) { Object.keys(resourceFields).forEach((key) => { const type = resourceFields[key].type; if ( type.startsWith('map[') ) { const t = type.slice(4, type.length - 1); const s = get(this, 'globalStore').getById('schema', t.toLowerCase()); if ( s ) { const underlineKey = camelToUnderline(key); if ( EXCLUDED_KEYS.indexOf(underlineKey) === -1 ) { EXCLUDED_KEYS.push(underlineKey); } } } }); }, getResourceFields(type) { const schema = get(this, 'globalStore').getById('schema', type.toLowerCase()); let resourceFields = null; if ( schema ) { resourceFields = get(schema, 'resourceFields'); this.findExcludedKeys(resourceFields); } return resourceFields; }, getFieldValue(field, type) { let resourceFields; const out = {}; if ( type.startsWith('map[') ) { type = type.slice(4, type.length - 1); resourceFields = this.getResourceFields(type); if ( resourceFields ) { if ( field ) { Object.keys(field).forEach((key) => { out[camelToUnderline(key)] = this.getFieldValue(field[key], type); }); return out; } else { return null; } } else { if ( field ) { Object.keys(field).forEach((key) => { out[camelToUnderline(key)] = field[key]; }); return out; } else { return null; } } } else if ( type.startsWith('array[') ) { type = type.slice(6, type.length - 1); resourceFields = this.getResourceFields(type); if ( resourceFields ) { return field ? field.map((item) => this.getFieldValue(item, type)) : null; } else { return field ? field.map((item) => item) : null; } } else { resourceFields = this.getResourceFields(type); if ( resourceFields ) { Object.keys(resourceFields).forEach((key) => { if ( !isEmpty(field) && (typeof field !== 'object' || Object.keys(field).length ) ) { out[camelToUnderline(key, type !== 'rkeConfigServices')] = this.getFieldValue(field[key], resourceFields[key].type); } }); return out; } else { return field; } } }, getSupportedFields(source, tragetField, excludeFields = []) { const out = {}; const resourceFields = this.getResourceFields(tragetField); Object.keys(resourceFields).filter((key) => resourceFields[key].update && excludeFields.indexOf(key) === -1).forEach((key) => { const field = get(source, key); const type = resourceFields[key].type; const value = this.getFieldValue(field, type); out[camelToUnderline(key)] = value; }); return out; }, initNonTemplateCluster() { if ( get(this, 'isNew') ) { this.createRkeConfigWithDefaults(); } else { this.initPrivateRegistries(); } this.initBackupConfigs(); scheduleOnce('afterRender', () => { this.initRootDockerDirectory(); }); }, initTemplateCluster() { this.initBackupConfigs(); this.initClusterTemplateQuestions(); }, initNodeCounts() { const counts = {}; let initialVersion = get(this, 'config.kubernetesVersion'); set(this, 'existingNodes', this.globalStore.all('node')); this.globalStore.findAll('node').then((all) => { all.forEach((node) => { const id = get(node, 'clusterId'); counts[id] = (counts[id] || 0) + 1; }); this.notifyPropertyChange('initialNodeCounts'); }); if (this.isEdit && !isEmpty(get(this, 'cluster.appliedSpec.rancherKubernetesEngineConfig.kubernetesVersion'))) { initialVersion = this.cluster.appliedSpec.rancherKubernetesEngineConfig.kubernetesVersion; } setProperties(this, { initialVersion, initialNodeCounts: counts, }) }, initPrivateRegistries() { const config = get(this, 'config'); if ( get(config, 'privateRegistries.length') > 0 ) { const registry = get(config, 'privateRegistries.firstObject'); setProperties(this, { registry: 'custom', registryUrl: get(registry, 'url'), registryUser: get(registry, 'user'), registryPass: get(registry, 'password'), }); } }, createRkeConfigWithDefaults() { const { globalStore, versionChoices, } = this; const defaultVersion = get(this, `settings.${ C.SETTING.VERSION_SYSTEM_K8S_DEFAULT_RANGE }`); const satisfying = maxSatisfying(versionChoices, defaultVersion); const out = {}; const rkeConfig = globalStore.createRecord({ type: 'rancherKubernetesEngineConfig', ignoreDockerVersion: true, kubernetesVersion: satisfying, authentication: globalStore.createRecord({ type: 'authnConfig', strategy: 'x509', }), network: globalStore.createRecord({ type: 'networkConfig', plugin: 'canal', options: { flannel_backend_type: DEFAULT_BACKEND_TYPE, }, }), ingress: globalStore.createRecord({ type: 'ingressConfig', provider: 'nginx', }), monitoring: globalStore.createRecord({ type: 'monitoringConfig', provider: 'metrics-server', }), services: globalStore.createRecord({ type: 'rkeConfigServices', kubeApi: globalStore.createRecord({ type: 'kubeAPIService', podSecurityPolicy: false, serviceNodePortRange: '30000-32767', }), etcd: globalStore.createRecord({ type: 'etcdService', snapshot: false, backupConfig: globalStore.createRecord({ type: 'backupConfig', enabled: true, }), extraArgs: { 'heartbeat-interval': 500, 'election-timeout': 5000 }, }), }), }); setProperties(out, { rancherKubernetesEngineConfig: rkeConfig, enableNetworkPolicy: false }); if (this.isNew) { set(out, 'localClusterAuthEndpoint', globalStore.createRecord({ type: 'localClusterAuthEndpoint', enabled: true })); } scheduleOnce('afterRender', () => { setProperties(this.primaryResource, out); this.notifyPropertyChange('config'); }); }, migrateLegacyEtcdSnapshotSettings() { const { config } = this; let { retention, creation, backupConfig } = config.services.etcd; let creationMatch = creation.match(/^((\d+)h)?((\d+)m)?((\d+)s)?$/); let momentReady = [creationMatch[2], creationMatch[4], creationMatch[6]]; if (momentReady[1] && momentReady[1] < 30) { // round min down since new settting is in intval hours momentReady[1] = 0; } else if (momentReady[1] && momentReady[1] >= 30) { // round up to the nearest hour momentReady[0] = parseInt(momentReady[0], 10) + 1; momentReady[1] = 0; } if (( !momentReady[0] || momentReady[0] === 0 ) && momentReady[1] === 0) { // if both hours and min are zero set hour to 1; momentReady[0] = 1; } let toMoment = { hours: momentReady[0] ? momentReady[0] : 0, minutes: momentReady[1] ? momentReady[1] : 0, seconds: momentReady[2] ? momentReady[2] : 0, }; const parsedDurationAsHours = moment.duration(toMoment).hours(); setProperties(this, { legacyRetention: retention, hasLegacySnapshotSettings: true, }); if (backupConfig) { setProperties(config.services.etcd, { 'backupConfig.enabled': true, 'backupConfig.intervalHours': parsedDurationAsHours, snapshot: false, }); } else { backupConfig = this.globalStore.createRecord({ type: 'backupConfig', intervalHours: parsedDurationAsHours, enabled: true, }); setProperties(config.services.etcd, { backupConfig, snapshot: false, }); } }, initRootDockerDirectory() { set(this, 'defaultDockerRootDir', get(this.globalStore.getById('schema', 'cluster').getCreateDefaults(), 'dockerRootDir')) }, initBackupConfigs() { const etcd = get(this, 'config.services.etcd'); if (etcd) { if (etcd.snapshot) { set(this, 'backupStrategy', 'local'); this.migrateLegacyEtcdSnapshotSettings(); } else if (etcd.backupConfig && etcd.backupConfig.s3BackupConfig) { set(this, 'backupStrategy', 's3'); } else if (!etcd.snapshot && !etcd.backupConfig) { const backupConfig = get(this, 'globalStore').createRecord({ enabled: false, type: 'backupConfig', }); set(etcd, 'backupConfig', backupConfig) } else { set(this, 'backupStrategy', 'local'); } } }, initClusterTemplateQuestions() { let { clusterTemplateQuestions, primaryResource, } = this; if (clusterTemplateQuestions && clusterTemplateQuestions.length > 0) { clusterTemplateQuestions.forEach((question) => { let path = question.variable; if (!this.isEdit && !question.variable.includes('uiOverride') && question.default) { deepSet(primaryResource, path, question.default); } set(question, 'primaryResource', primaryResource); defineProperty(question, 'default', alias(`primaryResource.${ path }`)); }); } }, checkKubernetesVersionSemVer() { let { clusterTemplateCreate = false, config : { kubernetesVersion }, model: { clusterTemplateRevision = {} } } = this; let questions = []; if (clusterTemplateRevision && clusterTemplateRevision.questions) { questions = clusterTemplateRevision.questions; } const kubernetesVersionOverrideExists = questions.findBy('variable', 'rancherKubernetesEngineConfig.kubernetesVersion'); if (clusterTemplateCreate) { if (kubernetesVersionOverrideExists) { if (kubernetesVersionOverrideExists.satisfies) { // we have a user defined override and this means we dont need satisfies because it can be anything delete kubernetesVersionOverrideExists.satisfies; } } else { if (kubernetesVersion.endsWith('x')) { this.send('addOverride', true, { path: 'rancherKubernetesEngineConfig.kubernetesVersion' }, true); } } } }, parseKubernetesVersionSemVer(cluster, question) { let { kubernetesVersion } = cluster.rancherKubernetesEngineConfig; let coercedVersion = Semver.coerce(kubernetesVersion); let satisfies = `>=${ coercedVersion.major }.${ coercedVersion.minor }.${ coercedVersion.patch } <${ coercedVersion.major }.${ coercedVersion.minor + 1 }` set(question, 'satisfies', satisfies); return question; }, });