import Component from '@ember/component'; import { computed, get, observer, set, setProperties } from '@ember/object'; import { equal } from '@ember/object/computed'; import { on } from '@ember/object/evented'; import { next } from '@ember/runloop'; import { inject as service } from '@ember/service'; import { isEmpty } from '@ember/utils'; import { Promise } from 'rsvp'; import { coerce, minor } from 'semver'; import { INSTANCE_TYPES } from 'shared/utils/amazon'; import { DEFAULT_NODE_GROUP_CONFIG } from 'ui/models/cluster'; import layout from './template'; export default Component.extend({ intl: service(), globalStore: service(), layout, classNames: ['row', 'mb-20'], instanceTypes: INSTANCE_TYPES, defaultNodeGroupConfig: DEFAULT_NODE_GROUP_CONFIG, clusterConfig: null, keyPairs: null, mode: null, model: null, nodeGroupsVersionCollection: null, originalCluster: null, versions: null, launchTemplates: null, allSelectedTemplateVersions: null, nodeInstanceRoles: null, clusterSaving: false, clusterSaved: false, nameIsEditable: true, showNodeUpgradePreventionReason: false, upgradeVersion: false, refreshResourceInstanceTags: true, // simply used for reinit'ing the resource instance tags key value component rather than add weird logic to recomput to the component editing: equal('mode', 'edit'), init() { this._super(...arguments); if (!this.launchTemplates) { set(this, 'launchTemplates', []); } if (this.editing) { if (!isEmpty(this.model.nodegroupName)) { set(this, 'nameIsEditable', false); } } }, actions: { setTags(section) { if (this.isDestroyed || this.isDestroying) { return; } set(this, 'model.tags', section); }, setLabels(section) { if (this.isDestroyed || this.isDestroying) { return; } set(this, 'model.labels', section); }, setResourceTags(section) { if (this.isDestroyed || this.isDestroying) { return; } set(this, 'resourceTags', section); }, }, instanceType: computed('defaultNodeGroupConfig.instanceType', 'isNoLaunchTemplate', 'model.instanceType', 'selectedTemplateVersionInfo.LaunchTemplateData.InstanceType', 'selectedTemplateVersionInfo.LaunchTemplateData.InstanceMarketOptions.MarketType', { get() { let instanceType = this?.model?.instanceType; const { selectedTemplateVersionInfo, isNoLaunchTemplate } = this; if (isNoLaunchTemplate) { return instanceType; } else if (selectedTemplateVersionInfo) { const LaunchTemplateData = selectedTemplateVersionInfo?.LaunchTemplateData; const MarketType = LaunchTemplateData?.InstanceMarketOptions?.MarketType; const InstanceType = LaunchTemplateData?.InstanceType ? LaunchTemplateData?.InstanceType : this.defaultNodeGroupConfig.instanceType; if (!isEmpty(MarketType) && MarketType === 'spot') { return null; } return InstanceType; } return instanceType; }, set(_key, instanceType) { const { selectedTemplateVersionInfo, isNoLaunchTemplate, isRancherLaunchTemplate } = this; if (isNoLaunchTemplate || isRancherLaunchTemplate) { set(this, 'model.instanceType', instanceType) } else if (selectedTemplateVersionInfo) { const { LaunchTemplateData: { InstanceMarketOptions: { MarketType } } } = selectedTemplateVersionInfo; if (!isEmpty(MarketType) && MarketType === 'spot') { set(this, 'model.instanceType', instanceType); } } return instanceType; } }), imageId: computed('defaultNodeGroupConfig.imageId', 'isNoLaunchTemplate', 'model.imageId', 'selectedTemplateVersionInfo.LaunchTemplateData.ImageId', { get() { let imageId = this?.model?.imageId; const { selectedTemplateVersionInfo, isNoLaunchTemplate } = this; if (isNoLaunchTemplate) { return imageId; } else if (selectedTemplateVersionInfo) { const { LaunchTemplateData: { ImageId = this.defaultNodeGroupConfig.imageId } } = selectedTemplateVersionInfo; return ImageId ?? this.defaultNodeGroupConfig.imageId; } return imageId; }, set(_key, imageId) { const { isNoLaunchTemplate, isRancherLaunchTemplate } = this; if (isNoLaunchTemplate || isRancherLaunchTemplate) { set(this, 'model.imageId', imageId) } return imageId; } }), diskSize: computed('defaultNodeGroupConfig.diskSize', 'isNoLaunchTemplate', 'model.diskSize', 'selectedTemplateVersionInfo.LaunchTemplateData.BlockDeviceMappings.[]', { get() { let diskSize = this?.model?.diskSize; const { selectedTemplateVersionInfo, isNoLaunchTemplate } = this; if (isNoLaunchTemplate) { return diskSize; } else if (selectedTemplateVersionInfo) { const blockDeviceMappings = selectedTemplateVersionInfo?.LaunchTemplateData?.BlockDeviceMappings?.firstObject ?? {}; const DiskSize = blockDeviceMappings?.Ebs?.VolumeSize ?? this.defaultNodeGroupConfig.diskSize; return DiskSize ?? this.defaultNodeGroupConfig.diskSize; } return diskSize; }, set(_key, diskSize) { const { isNoLaunchTemplate, isRancherLaunchTemplate } = this; if (isNoLaunchTemplate || isRancherLaunchTemplate) { set(this, 'model.diskSize', diskSize) } return diskSize; } }), ec2SshKey: computed('defaultNodeGroupConfig.ec2SshKey', 'isNoLaunchTemplate', 'model.ec2SshKey', 'selectedTemplateVersionInfo.LaunchTemplateData.KeyName', { get() { let ec2SshKey = this?.model?.ec2SshKey; const { selectedTemplateVersionInfo, isNoLaunchTemplate } = this; if (isNoLaunchTemplate) { return ec2SshKey; } else if (selectedTemplateVersionInfo) { const { LaunchTemplateData: { KeyName = this.defaultNodeGroupConfig.ec2SshKey } } = selectedTemplateVersionInfo; return KeyName ?? this.defaultNodeGroupConfig.ec2SshKey; } return ec2SshKey; }, set(_key, ec2SshKey) { const { isNoLaunchTemplate, isRancherLaunchTemplate } = this; if (isNoLaunchTemplate || isRancherLaunchTemplate) { set(this, 'model.ec2SshKey', ec2SshKey) } return ec2SshKey; } }), userData: computed('defaultNodeGroupConfig.userData', 'isNoLaunchTemplate', 'model.userData', 'selectedTemplateVersionInfo.LaunchTemplateData.UserData', { get() { let userData = this?.model?.userData; const { selectedTemplateVersionInfo, isNoLaunchTemplate } = this; if (isNoLaunchTemplate) { return userData; } else if (selectedTemplateVersionInfo) { let { LaunchTemplateData: { UserData = this.defaultNodeGroupConfig.userData } } = selectedTemplateVersionInfo; try { UserData = atob(UserData); } catch (_err) { } return UserData ?? this.defaultNodeGroupConfig.userData; } return userData; }, set(_key, userData) { const { isNoLaunchTemplate, isRancherLaunchTemplate } = this; if (isNoLaunchTemplate || isRancherLaunchTemplate) { set(this, 'model.userData', userData) } return userData; } }), resourceTags: computed('defaultNodeGroupConfig.resourceTags', 'isNoLaunchTemplate', 'model.resourceTags', 'selectedTemplateVersionInfo.LaunchTemplateData.TagSpecifications.[]', { get() { let resourceTags = this?.model?.resourceTags; const { selectedTemplateVersionInfo, isNoLaunchTemplate } = this; if (isNoLaunchTemplate) { return resourceTags; } else if (selectedTemplateVersionInfo) { const resourceInstanceTags = ( selectedTemplateVersionInfo?.LaunchTemplateData?.TagSpecifications ?? []).findBy('ResourceType', 'instance'); const resourceTags = {}; (resourceInstanceTags?.Tags ?? []).forEach((tag) => set(resourceTags, get(tag, 'Key'), get(tag, 'Value'))); return resourceTags; } return resourceTags; }, set(_key, resourceTags) { const { isNoLaunchTemplate, isRancherLaunchTemplate } = this; if (isNoLaunchTemplate || isRancherLaunchTemplate) { set(this, 'model.resourceTags', resourceTags) } return resourceTags; } }), isRancherLaunchTemplate: computed('model.{launchTemplate,nodegroupName}', 'originalCluster.eksStatus.managedLaunchTemplateID', function() { const { originalCluster, model } = this; const { launchTemplate } = model; const eksStatus = get((originalCluster ?? {}), 'eksStatus') || {}; const { managedLaunchTemplateID = null, managedLaunchTemplateVersions = {} } = eksStatus; const matchedManagedVersion = get(( managedLaunchTemplateVersions ?? {} ), this.model.nodegroupName); if (isEmpty(launchTemplate) && !isEmpty(managedLaunchTemplateID) && !isEmpty(matchedManagedVersion)) { return true; } return false; }), isUserLaunchTemplate: computed('model.launchTemplate', 'originalCluster.eksStatus.managedLaunchTemplateID', function() { const { model } = this; const { launchTemplate } = model; if (!isEmpty(launchTemplate) && !isEmpty(launchTemplate?.id) && !isEmpty(launchTemplate?.version)) { return true; } return false; }), isNoLaunchTemplate: computed('isRancherLaunchTemplate', 'isUserLaunchTemplate', function() { return !this.isRancherLaunchTemplate && !this.isUserLaunchTemplate; }), filteredLaunchTemplates: computed('launchTemplates.[]', function() { const { launchTemplates } = this; if (isEmpty(launchTemplates)) { return []; } return launchTemplates.filter(({ LaunchTemplateName }) => !LaunchTemplateName.includes('rancher-managed-lt') ).sortBy('LaunchTemplateName'); }), selectedTemplateVersionInfo: computed('model.launchTemplate.{id,version}', 'allSelectedTemplateVersions.[]', function() { const { model, allSelectedTemplateVersions } = this; const version = get(model, 'launchTemplate.version'); if (isEmpty(model.launchTemplate) || isEmpty(version)) { return null; } const match = (allSelectedTemplateVersions || []).findBy('VersionNumber', parseInt(version, 10)); if (match) { return match; } return null; }), selectedLaunchTemplateVersion: computed('model.launchTemplate.version', 'model.launchTemplate.id', { get() { return get(this, 'model.launchTemplate.version') ? get(this, 'model.launchTemplate.version').toString() : null; }, set(_key, value) { set(this, 'model.launchTemplate.version', parseInt(value, 10)); this.loadTemplateVersionInfo(); return value; }, }), selectedLaunchTemplateVersions: computed('model.launchTemplate.{id,name,version}', 'launchTemplates', function() { const { model, launchTemplates } = this; const { launchTemplate } = model; if (isEmpty(launchTemplate) || isEmpty(get(launchTemplate, 'id'))) { return []; } const match = launchTemplates.findBy('LaunchTemplateId', launchTemplate.id); if (match) { // this lets us create a range of values 1...XX because the launch template only gives us the 1st and latest numbers but we want all for the version select // ++ver -> zero based array so we need to +1 that value to match a non-zero based version number system return Array.from(Array(match.LatestVersionNumber).keys()).map((ver) => ({ label: `${ ++ver }` })); } return []; }), selectedLaunchTemplate: computed('model.launchTemplate', 'filteredLaunchTemplates.[]', { get() { const launchTemplate = get(this, 'model.launchTemplate') ?? false; if (launchTemplate) { const out = this.filteredLaunchTemplates.findBy('LaunchTemplateId', launchTemplate.id); return isEmpty(out) ? null : out; } return null; }, set(_key, launchTemplateId) { const launchTemplate = this.filteredLaunchTemplates.findBy('LaunchTemplateId', launchTemplateId); const { LaunchTemplateId: id, LaunchTemplateName: name, DefaultVersionNumber: version } = launchTemplate ?? {}; if (isEmpty(launchTemplate)) { set(this, 'model.launchTemplate', null); setProperties(this, { diskSize: this.defaultNodeGroupConfig.diskSize, ec2SshKey: this.defaultNodeGroupConfig.ec2SshKey, imageId: this.defaultNodeGroupConfig.imageId, instanceType: this.defaultNodeGroupConfig.instanceType, requestSpotInstances: this.defaultNodeGroupConfig.requestSpotInstances, resourceTags: this.defaultNodeGroupConfig.resourceTags, userData: this.defaultNodeGroupConfig.userData, }); } else { set(this, 'model.launchTemplate', { id, name, }); set(this, 'selectedLaunchTemplateVersion', version); } return launchTemplateId; } }), creating: computed('mode', function() { const { mode, originalCluster, model: { nodegroupName } } = this; if (mode === 'new') { return true; } const upstreamSpec = get(originalCluster, 'eksStatus.upstreamSpec'); const nodeGroups = upstreamSpec ? get(upstreamSpec, 'nodeGroups') : []; if (nodegroupName && nodeGroups.length >= 1) { if (nodeGroups.findBy('nodegroupName', nodegroupName)) { return false; } } return true; }), originalClusterVersion: computed('originalCluster.eksConfig.kubernetesVersion', 'originalCluster.eksStatus.upstreamSpec.kubernetesVersion', function() { if (!isEmpty(get(this, 'originalCluster.eksConfig.kubernetesVersion'))) { return get(this, 'originalCluster.eksConfig.kubernetesVersion'); } if (!isEmpty(get(this, 'originalCluster.eksStatus.upstreamSpec.kubernetesVersion'))) { return get(this, 'originalCluster.eksStatus.upstreamSpec.kubernetesVersion'); } return ''; }), upgradeAvailable: computed('clusterConfig.kubernetesVersion', 'mode', 'model.version', 'originalClusterVersion', 'showNodeUpgradePreventionReason', function() { const originalClusterVersion = get(this, 'originalClusterVersion'); const clusterVersion = get(this, 'clusterConfig.kubernetesVersion'); const nodeVersion = get(this, 'model.version'); const mode = get(this, 'mode'); const initalClusterMinorVersion = parseInt(minor(coerce(clusterVersion)), 10); const initalNodeMinorVersion = parseInt(minor(coerce(nodeVersion)), 10); const diff = initalClusterMinorVersion - initalNodeMinorVersion; if (mode === 'edit') { // we must upgrade the cluster first if (originalClusterVersion !== clusterVersion) { set(this, 'showNodeUpgradePreventionReason', true); return false; } } if (diff === 0 && get(this, 'showNodeUpgradePreventionReason')) { set(this, 'showNodeUpgradePreventionReason', false); } return diff === 1; }), showGPUWarning: computed('model.launchTemplate.{id,version}', 'selectedTemplateVersionInfo', function() { const { model, selectedTemplateVersionInfo } = this; const ltId = get(model, 'launchTemplate.id'); const ltVersion = get(model, 'launchTemplate.version'); const imageId = selectedTemplateVersionInfo ? get(selectedTemplateVersionInfo, 'LaunchTemplateData.ImageId') : undefined; return ltId && ltVersion && selectedTemplateVersionInfo && imageId; }), requestedSpotInstances: on('init', observer('model.requestSpotInstances', 'selectedTemplateVersionInfo.LaunchTemplateData.InstanceMarketOptions.MarketType', function() { const { model } = this; if (get(model, 'requestSpotInstances')) { set(this, 'instanceType', null); } else if (!get(model, 'requestSpotInstances') && get(model, 'instanceType') === null) { set(this, 'instanceType', 't3.medium'); } })), clusterVersionDidChange: on('init', observer('clusterConfig.kubernetesVersion', function() { const { clusterConfig, editing } = this; if (get(clusterConfig, 'kubernetesVersion') && !editing) { set(this, 'model.version', clusterConfig.kubernetesVersion); } })), shouldUpgradeVersion: on('init', observer('upgradeVersion', function() { const { upgradeVersion } = this; const clusterVersion = get(this, 'clusterConfig.kubernetesVersion'); const nodeVersion = get(this, 'model.version'); if (upgradeVersion && clusterVersion !== nodeVersion) { set(this, 'model.version', clusterVersion); } })), nodeRoleOptions: computed('intl', 'nodeInstanceRoles.[]', function(){ const { nodeInstanceRoles } = this; return [{ RoleName: this.intl.t('nodeGroupRow.nodeInstanceRole.defaultOption'), RoleId: '' }, ...nodeInstanceRoles] }), async loadTemplateVersionInfo() { if (!this.clusterSaving && !this.clusterSaved) { const { launchTemplate = {} } = this.model; let defaults = { ...this.defaultNodeGroupConfig }; // in this case we dont want the defaults for nodegroup items delete defaults.nodegroupName; delete defaults.maxSize; delete defaults.minSize; delete defaults.desiredSize; if (isEmpty(launchTemplate)) { set(this, 'refreshResourceInstanceTags', false); next(() => { setProperties(this.model, defaults); set(this, 'refreshResourceInstanceTags', true) }); } else if ( !isEmpty(get(launchTemplate, 'id')) ) { try { const versions = await this.listTemplateVersions(); const match = versions.findBy('VersionNumber', parseInt(launchTemplate.version, 10)); // newselect doesn't handle numbers as values very well const { LaunchTemplateData: launchTemplateData } = match; const overrides = { imageId: get(launchTemplateData, 'ImageId') ?? null, instanceType: get(launchTemplateData, 'InstanceType') ?? this.defaultNodeGroupConfig.instanceType, diskSize: get(launchTemplateData, 'BlockDeviceMappings.firstObject.Ebs.VolumeSize') ?? this.defaultNodeGroupConfig.diskSize, ec2SshKey: get(launchTemplateData, 'KeyName') ?? null, userData: isEmpty(get(launchTemplateData, 'UserData')) ? null : atob(get(launchTemplateData, 'UserData')), }; defaults = Object.assign({}, defaults, overrides); if (get(launchTemplateData, 'InstanceMarketOptions.MarketType') && get(launchTemplateData, 'InstanceMarketOptions.MarketType') === 'spot') { set(defaults, 'requestSpotInstances', true); set(this, 'instanceType', null); } if ( !isEmpty(get(launchTemplateData, 'TagSpecifications')) ) { const resourceInstanceTags = get(launchTemplateData, 'TagSpecifications').findBy('ResourceType', 'instance'); if (!isEmpty(resourceInstanceTags) && !isEmpty(get(resourceInstanceTags, 'Tags'))) { set(defaults, 'resourceTags', {}); resourceInstanceTags.Tags.forEach((tag) => set(defaults.resourceTags, get(tag, 'Key'), get(tag, 'Value'))); } } set(this, 'refreshResourceInstanceTags', false); next(() => { setProperties(this.model, defaults); set(this, 'allSelectedTemplateVersions', versions); set(this, 'refreshResourceInstanceTags', true) } ); } catch (err) { } } } }, listTemplateVersions() { const { launchTemplate } = this.model; const match = this.launchTemplates.findBy('LaunchTemplateId', launchTemplate.id); return new Promise((resolve, reject) => { const ec2 = new AWS.EC2(this.authCreds()); const maxVersion = match.LatestVersionNumber; ec2.describeLaunchTemplateVersions({ LaunchTemplateId: launchTemplate.id, MaxVersion: maxVersion.toString(), MinVersion: '1', }, (err, data) => { if (err) { reject(err); } resolve(data.LaunchTemplateVersions); }); }) }, removeNodeGroup() { throw new Error('remove node group action is required!'); }, });