import Component from '@ember/component'; import { computed, get, observer, set, setProperties } from '@ember/object'; import { alias, equal } from '@ember/object/computed'; import { inject as service } from '@ember/service'; import { isEmpty } from '@ember/utils'; import $ from 'jquery'; import { Promise as EmberPromise } from 'rsvp'; import ChildHook from 'shared/mixins/child-hook'; import ViewNewEdit from 'shared/mixins/view-new-edit'; import { loadStylesheet, proxifyUrl } from 'shared/utils/load-script'; import layout from './template'; const MEMBER_CONFIG = { type: 'clusterRoleTemplateBinding', }; const BUILD_IN_UI = ['tencentkubernetesengine', 'huaweicontainercloudengine', 'oraclecontainerengine', 'linodekubernetesengine']; const V2_DRIVERS = ['amazoneksv2', 'googlegkev2', 'azureaksv2']; export default Component.extend(ViewNewEdit, ChildHook, { globalStore: service(), intl: service(), access: service(), cookies: service(), router: service(), settings: service(), layout, step: 1, initialProvider: null, memberConfig: MEMBER_CONFIG, newCluster: false, needReloadSchema: false, reloadingSchema: false, schemaReloaded: false, applyClusterTemplate: false, routeLoading: false, isPostSave: false, showClassicLauncher: false, nodePoolErrors: null, clusterTemplateRevisionId: null, cluster: alias('model.cluster'), originalCluster: alias('model.originalCluster'), primaryResource: alias('model.cluster'), clusterState: alias('originalCluster.state'), isCustom: equal('driverInfo.nodeWhich', 'custom'), isK3sCluster: equal('model.cluster.driver', 'k3s'), isRke2Cluster: equal('model.cluster.driver', 'rke2'), init() { this._super(...arguments); // On edit pass in initialProvider, for create just set provider directly const initialProvider = get(this, 'initialProvider'); if (this.cookies.get('classicClusterLaunch')) { set(this, 'showClassicLauncher', true); } else { set(this, 'showClassicLauncher', false); } if ( initialProvider ) { set(this, 'provider', initialProvider); } if ( isEmpty(get(this, 'cluster.id')) ){ set(this, 'newCluster', true); } this.router.on('routeWillChange', (/* transition */) => { if ( !this.isDestroyed || !this.isDestroying ) { set(this, 'routeLoading', true); } }); this.router.on('routeDidChange', (/* transition */) => { if ( !this.isDestroyed || !this.isDestroying ) { set(this, 'routeLoading', false); } }); }, actions: { updateFromYaml(newOpts) { let { primaryResource } = this; let _cachedName; /** * If a user switches to the yaml entry view before they put a name in the cluster name input * and then they put a name in the input rather that the yaml, when they click save we overwrite * the name with the empty (or remove if not included) value. * Ensure the name they've entered in the UI is used. This will not affect name entry in yaml. */ if (primaryResource.name) { _cachedName = primaryResource.name; } if (this.isEdit) { primaryResource.merge(newOpts); } else { primaryResource.replaceWith(newOpts); } if (isEmpty(primaryResource.name) && _cachedName) { set(primaryResource, 'name', _cachedName); } }, clickNext() { $('BUTTON[type="submit"]').click(); }, close(saved) { if (this.close) { set(this, 'isPostSave', false); this.close(saved); } }, setNodePoolErrors(errors) { set(this, 'nodePoolErrors', errors); }, }, reloadSchema: observer('needReloadSchema', function() { if ( !get(this, 'reloadingSchema') && get(this, 'needReloadSchema') ) { set(this, 'reloadingSchema', true); get(this, 'globalStore').findAll('schema', { url: '/v3/schemas', forceReload: true }).then(() => { setProperties(this, { schemaReloaded: true, reloadingSchema: false }); }); } }), agentEnvVars: computed('cluster.agentEnvVars.[]', { get() { if (!this.cluster?.agentEnvVars) { // if undefined two way binding doesn't seem to work set(this, 'cluster.agentEnvVars', []); } return get(this, 'cluster.agentEnvVars') || []; }, set(_key, value) { set(this, 'cluster.agentEnvVars', value); return value; }, }), isEksClusterPending: computed('clusterState', 'provider', function() { const { clusterState, provider } = this; if (['pending', 'waiting'].includes(clusterState) && provider === 'amazoneksv2') { return true; } return false; }), kontainerDrivers: computed('model.kontainerDrivers.@each.{id,state}', function() { let nope = ['import', 'rancherkubernetesengine']; let kDrivers = get(this, 'model.kontainerDrivers') || []; let builtIn = kDrivers.filter( (d) => d.state === 'active' && (d.builtIn || BUILD_IN_UI.indexOf(d.id) > -1) && !nope.includes(d.id)); let custom = kDrivers.filter( (d) => d.state === 'active' && !d.builtIn && d.hasUi); return { builtIn, custom } }), providerChoices: computed( 'app.proxyEndpoint', 'cluster.rancherKubernetesEngineConfig', 'intl.locale', 'isEdit', 'kontainerDrivers.[]', 'model.nodeDrivers', 'nodeDrivers.[]', 'schemaReloaded', function() { const { kontainerDrivers, intl } = this; const { builtIn, custom } = kontainerDrivers; let out = [ { name: 'googlegke', driver: 'googlegke', kontainerId: 'googlekubernetesengine', }, { displayName: 'Google GKE', driver: 'gke', name: 'googlegkev2', nodePool: false, nodeWhich: 'gke', preSave: false, postSave: true, }, { name: 'amazoneks', driver: 'amazoneks', kontainerId: 'amazonelasticcontainerservice', }, { displayName: 'Amazon EKS', driver: 'eks', name: 'amazoneksv2', nodePool: false, nodeWhich: 'eks', preSave: false, postSave: true, }, { name: 'azureaks', driver: 'azureaks', kontainerId: 'azurekubernetesservice', }, { displayName: 'Azure AKS', driver: 'aks', name: 'azureaksv2', nodePool: false, nodeWhich: 'aks', preSave: false, postSave: true, }, { name: 'tencenttke', driver: 'tencenttke', kontainerId: 'tencentkubernetesengine' }, { name: 'huaweicce', driver: 'huaweicce', kontainerId: 'huaweicontainercloudengine' }, { name: 'oracleoke', driver: 'oracleoke', kontainerId: 'oraclecontainerengine' }, { name: 'linodelke', driver: 'linodelke', kontainerId: 'linodekubernetesengine' }, ]; out = out.filter( (o) => builtIn.findBy('id', o.kontainerId) || V2_DRIVERS.includes(o.name)); if (custom.length > 0) { custom.forEach( (c) => { const { name } = c; const configName = `${ name }EngineConfig`; // need the hyph name const driverEngineSchema = get(this, 'globalStore').getById('schema', configName.toLowerCase()); if ( driverEngineSchema ) { out.pushObject({ displayName: get(c, 'displayName'), driver: get(c, 'name'), kontainerId: get(c, 'id'), name: get(c, 'name'), genericIcon: true, // @TODO should have a way for drivers to provide an icon }); } else { set(this, 'needReloadSchema', true); } }); } get(this, 'model.nodeDrivers').filterBy('state', 'active').sortBy('name').forEach((driver) => { const name = get(driver, 'name'); const hasUi = get(driver, 'hasUi'); const hasIcon = get(driver, 'hasBuiltinIconOnly'); const uiUrl = get(driver, 'uiUrl'); const configName = `${ name }Config`; const driverSchema = get(this, 'globalStore').getById('schema', configName.toLowerCase()); if ( uiUrl ) { const cssUrl = proxifyUrl(uiUrl.replace(/\.js$/, '.css'), get(this, 'app.proxyEndpoint')); loadStylesheet(cssUrl, `driver-ui-css-${ name }`); } if ( driverSchema ) { out.push({ name, driver: 'rke', genericIcon: !hasUi && !hasIcon, nodeComponent: hasUi ? name : 'generic', nodeWhich: name, nodePool: true, }); } else { set(this, 'needReloadSchema', true); } }), out.push({ name: 'custom', driver: 'rke', nodeWhich: 'custom', nodePool: true, preSave: true, postSave: true, }); out.push({ name: 'import', driver: 'import', preSave: true, postSave: true }); out.push({ name: 'imported', driver: 'import', preSave: true, postSave: true }); out.push({ name: 'importeks', driver: 'import-eks', preSave: false, postSave: true, }); out.push({ name: 'importgke', driver: 'import-gke', preSave: false, postSave: true, }); out.push({ name: 'importaks', driver: 'import-aks', preSave: false, postSave: true, }); out.push({ name: 'k3s', driver: 'import', preSave: true }); out.push({ name: 'rke2', driver: 'import', preSave: true }); out.forEach((driver) => { const key = `clusterNew.${ driver.name }.label`; if ( !get(driver, 'displayName') && intl.exists(key) ) { set(driver, 'displayName', intl.t(key)); } }); if ( get(this, 'isEdit') && get(this, 'cluster.rancherKubernetesEngineConfig') ) { out = out.filterBy('driver', 'rke'); } out.sortBy('name'); return out; }), driverInfo: computed('provider', 'providerChoices', 'router.currentRoute.queryParams', function() { let name = get(this, 'provider'); const { router } = this; const importProvider = get(router, 'currentRoute.queryParams.importProvider'); if (name === 'import') { if (isEmpty(importProvider)) { return null; } else if (importProvider === 'eks') { name = 'importeks' } else if (importProvider === 'gke') { name = 'importgke' } else if (importProvider === 'aks') { name = 'importaks' } } const choices = get(this, 'providerChoices'); const entry = choices.findBy('name', name); if ( entry ) { return { name: entry.name, displayName: entry.displayName, driverComponent: `cluster-driver/driver-${ entry.driver }`, nodeWhich: entry.nodeWhich, preSave: !!entry.preSave, postSave: !!entry.postSave, nodePool: entry.nodePool || false }; } return { name: 'unknown', displayName: 'unknown', driverComponent: null, nodeWhich: 'unknown', preSave: false, nodePool: false }; }), isImportedOther: computed('isK3sCluster', 'provider', 'router.currentRoute.queryParams.importProvider', function() { // This is a special case because k3s is treated as a special cluster and no longer looks imported by the time it's actve if (get(this, 'isK3sCluster')) { return true; } const name = get(this, 'provider'); const importProvider = get(this, 'router.currentRoute.queryParams.importProvider'); return name === 'import' && (!importProvider || importProvider === 'other'); }), showDriverComponent: computed('routeLoading', 'provider', 'router.currentRoute.queryParams', function() { const { driverInfo, provider, routeLoading, router, } = this; const importProvider = get(router, 'currentRoute.queryParams.importProvider'); if (routeLoading || isEmpty(driverInfo)) { return false; } if (provider === 'import') { if (!isEmpty(importProvider)) { return true; } else { return false; } } return true; }), doSave(opt) { opt = opt || {}; opt.qp = { '_replace': 'true' }; return this._super(opt); }, didSave() { const originalCluster = get(this, 'cluster'); // For edit, the cluster will already have the InitialRolesPopulated condition, so we don't need to wait for it // A new cluster will not and the condition will be added as part of the creation. // The local cluster is pre-cerated and does not have the condition set, so waiting for it in the edit case // will cause an eventual tiemout and error message - hence we don't wait for the condition in the edit case let waitPromise = originalCluster.waitForCondition('InitialRolesPopulated'); if (this.isEdit) { waitPromise = new EmberPromise((resolve) => { resolve() }); } return waitPromise.then(() => { return this.applyHooks().then(() => { const clone = originalCluster.clone(); setProperties(this, { cluster: clone, originalCluster, }); return clone; }); }); }, doneSaving(saved) { if ( get(this, 'step') === 1 && get(this, 'driverInfo.preSave') && !get(this, 'driverInfo.postSave') ) { const skipK3sImport = !isEmpty(saved?.provider) && saved.provider === 'k3s' && isEmpty(saved?.k3sConfig); if (skipK3sImport && this.originalCluster?.state !== 'pending') { if (this.close) { this.close(saved); } } else if (this.originalCluster?.state === 'pending') { setProperties(this, { step: 2, initialProvider: get(this, 'provider') }); } else { if (this.close) { this.close(saved); } } } else if (get(this, 'driverInfo.postSave')) { setProperties(this, { initialProvider: get(this, 'provider'), isPostSave: true, step: 2, }); } else { if (this.close) { this.close(saved); } } }, errorSaving(/* err */) { if (this.applyClusterTemplate && this.primaryResource._cachedConfig) { let { localClusterAuthEndpoint, rancherKubernetesEngineConfig, enableNetworkPolicy, defaultClusterRoleForProjectMembers, defaultPodSecurityPolicyTemplateId, } = this.primaryResource._cachedConfig; setProperties(this.primaryResource, { localClusterAuthEndpoint, rancherKubernetesEngineConfig, enableNetworkPolicy, defaultClusterRoleForProjectMembers, defaultPodSecurityPolicyTemplateId, }); } }, });