ui/app/models/cluster.js

1074 lines
35 KiB
JavaScript

import { get, set, computed, observer } from '@ember/object';
import { on } from '@ember/object/evented';
import { inject as service } from '@ember/service';
import Resource from '@rancher/ember-api-store/models/resource';
import { hasMany, reference } from '@rancher/ember-api-store/utils/denormalize';
import ResourceUsage from 'shared/mixins/resource-usage';
import Grafana from 'shared/mixins/grafana';
import { equal, alias } from '@ember/object/computed';
import { resolve } from 'rsvp';
import C from 'ui/utils/constants';
import { isEmpty, isEqual } from '@ember/utils';
import moment from 'moment';
import jsondiffpatch from 'jsondiffpatch';
import { isArray } from '@ember/array';
import Semver from 'semver';
const TRUE = 'True';
const CLUSTER_TEMPLATE_ID_PREFIX = 'cattle-global-data:';
const SCHEDULE_CLUSTER_SCAN_QUESTION_KEY = 'scheduledClusterScan.enabled';
export const DEFAULT_USER_DATA =
`MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="==MYBOUNDARY=="
--==MYBOUNDARY==
Content-Type: text/x-shellscript; charset="us-ascii"
#!/bin/bash
echo "Running custom user data script"
--==MYBOUNDARY==--\\`;
export const DEFAULT_NODE_GROUP_CONFIG = {
desiredSize: 2,
diskSize: 20,
ec2SshKey: '',
gpu: false,
imageId: null,
instanceType: 't3.medium',
labels: {},
maxSize: 2,
minSize: 2,
nodegroupName: '',
requestSpotInstances: false,
resourceTags: {},
spotInstanceTypes: [],
subnets: [],
tags: {},
type: 'nodeGroup',
userData: DEFAULT_USER_DATA,
};
export const DEFAULT_EKS_CONFIG = {
amazonCredentialSecret: '',
displayName: '',
imported: false,
kmsKey: '',
kubernetesVersion: '',
loggingTypes: [],
nodeGroups: [],
privateAccess: false,
publicAccess: true,
publicAccessSources: [],
region: 'us-west-2',
secretsEncryption: false,
securityGroups: [],
serviceRole: '',
subnets: [],
tags: {},
type: 'eksclusterconfigspec',
};
export default Resource.extend(Grafana, ResourceUsage, {
globalStore: service(),
growl: service(),
intl: service(),
router: service(),
scope: service(),
clusterRoleTemplateBindings: hasMany('id', 'clusterRoleTemplateBinding', 'clusterId'),
etcdbackups: hasMany('id', 'etcdbackup', 'clusterId'),
namespaces: hasMany('id', 'namespace', 'clusterId'),
nodePools: hasMany('id', 'nodePool', 'clusterId'),
nodes: hasMany('id', 'node', 'clusterId'),
projects: hasMany('id', 'project', 'clusterId'),
clusterScans: hasMany('id', 'clusterScan', 'clusterId'),
expiringCerts: null,
grafanaDashboardName: 'Cluster',
isMonitoringReady: false,
_cachedConfig: null,
clusterTemplate: reference('clusterTemplateId'),
clusterTemplateRevision: reference('clusterTemplateRevisionId'),
machines: alias('nodes'),
roleTemplateBindings: alias('clusterRoleTemplateBindings'),
isAKS: equal('driver', 'azureKubernetesService'),
isGKE: equal('driver', 'googleKubernetesEngine'),
runningClusterScans: computed.filterBy('clusterScans', 'isRunning', true),
conditionsDidChange: on('init', observer('enableClusterMonitoring', 'conditions.@each.status', function() {
if ( !get(this, 'enableClusterMonitoring') ) {
return false;
}
const conditions = get(this, 'conditions') || [];
const ready = conditions.findBy('type', 'MonitoringEnabled');
const status = ready && get(ready, 'status') === 'True';
if ( status !== get(this, 'isMonitoringReady') ) {
set(this, 'isMonitoringReady', status);
}
})),
clusterTemplateDisplayName: computed('clusterTemplate.{displayName,name}', 'clusterTemplateId', function() {
const displayName = get(this, 'clusterTemplate.displayName');
const clusterTemplateId = (get(this, 'clusterTemplateId') || '').replace(CLUSTER_TEMPLATE_ID_PREFIX, '');
return displayName || clusterTemplateId;
}),
clusterTemplateRevisionDisplayName: computed('clusterTemplateRevision.{displayName,name}', 'clusterTemplateRevisionId', function() {
const displayName = get(this, 'clusterTemplateRevision.displayName');
const revisionId = (get(this, 'clusterTemplateRevisionId') || '').replace(CLUSTER_TEMPLATE_ID_PREFIX, '')
return displayName || revisionId;
}),
isClusterTemplateUpgradeAvailable: computed('clusterTemplate.latestRevision', 'clusterTemplate.latestRevision.id', 'clusterTemplateRevision.id', function() {
const latestClusterTemplateRevisionId = get(this, 'clusterTemplate.latestRevision.id');
const currentClusterTemplateRevisionId = get(this, 'clusterTemplateRevision.id');
return latestClusterTemplateRevisionId
&& currentClusterTemplateRevisionId
&& currentClusterTemplateRevisionId !== latestClusterTemplateRevisionId;
}),
getAltActionDelete: computed('action.remove', function() { // eslint-disable-line
return get(this, 'canBulkRemove') ? 'delete' : null;
}),
hasSessionToken: computed('annotations', function() {
const sessionTokenLabel = `${ (get(this, 'annotations') || {})[C.LABEL.EKS_SESSION_TOKEN] }`;
let hasSessionToken = false;
if (sessionTokenLabel === 'undefined' || sessionTokenLabel === 'false') {
hasSessionToken = false;
} else {
hasSessionToken = true;
}
return hasSessionToken;
}),
canRotateCerts: computed('actionLinks.rotateCertificates', function() {
return !!this.actionLinks.rotateCertificates;
}),
canRotateEncryptionKey: computed(
'actionLinks.rotateEncryptionKey',
'etcdbackups.@each.created',
'rancherKubernetesEngineConfig.rotateEncryptionKey',
'rancherKubernetesEngineConfig.services.kubeApi.secretsEncryptionConfig.enabled',
'transitioning',
'isActive',
function() {
const acceptableTimeFrame = 360;
const {
actionLinks: { rotateEncryptionKey }, etcdbackups, rancherKubernetesEngineConfig
} = this;
const lastBackup = !isEmpty(etcdbackups) ? get(etcdbackups, 'lastObject') : undefined;
let diffInMinutes = 0;
if (this.transitioning !== 'no' || !this.isActive) {
return false;
}
if (isEmpty(rancherKubernetesEngineConfig)) {
return false;
} else {
const {
rotateEncryptionKey = false,
services: { kubeApi: { secretsEncryptionConfig = null } }
} = rancherKubernetesEngineConfig;
if (!!rotateEncryptionKey || isEmpty(secretsEncryptionConfig) || !get(secretsEncryptionConfig, 'enabled')) {
return false
}
}
if (lastBackup) {
diffInMinutes = moment().diff(lastBackup.created, 'minutes');
}
return rotateEncryptionKey && diffInMinutes <= acceptableTimeFrame;
}),
canBulkRemove: computed('action.remove', function() { // eslint-disable-line
return get(this, 'hasSessionToken') ? false : true;
}),
canSaveAsTemplate: computed('actionLinks.saveAsTemplate', 'isReady', 'clusterTemplateRevisionId', 'clusterTemplateId', function() {
let {
actionLinks,
isReady,
clusterTemplateRevisionId,
clusterTemplateId,
} = this;
if (!isReady) {
return false;
}
if (clusterTemplateRevisionId || clusterTemplateId) {
return false;
}
return !!actionLinks.saveAsTemplate;
}),
eksDisplayEksImport: computed('clusterProvider', 'eksConfig.privateAccess', 'eksStatus.upstreamSpec.privateAccess', function() {
const {
clusterProvider, eksConfig = {}, eksStatus = {}
} = this;
const publicAccess = get(eksStatus, 'upstreamSpec.publicAccess') || get(eksConfig, 'publicAccess') || true;
const privateAccess = get(eksStatus, 'upstreamSpec.privateAccess') || get(eksConfig, 'privateAccess') || false;
if (clusterProvider === 'amazoneksv2' && !publicAccess && privateAccess) {
return true;
}
return false;
}),
canShowAddHost: computed('nodes.[]', 'clusterProvider', 'eksConfig.privateAccess', 'eksStatus.upstreamSpec.privateAccess', function() {
const {
clusterProvider, eksConfig = {}, eksStatus = {}
} = this;
const publicAccess = get(eksStatus, 'upstreamSpec.publicAccess') || get(eksConfig, 'publicAccess') || true;
const privateAccess = get(eksStatus, 'upstreamSpec.privateAccess') || get(eksConfig, 'privateAccess') || false;
const ignored = ['custom', 'import', 'amazoneksv2'];
const nodes = get(this, 'nodes');
if (!ignored.includes(clusterProvider)) {
return false;
}
// private access requires the ability to run the import command on the cluster
if (clusterProvider === 'amazoneksv2' && !!publicAccess && privateAccess) {
return true;
} else if (clusterProvider !== 'amazoneksv2' && isEmpty(nodes)) {
return true;
}
return false;
}),
configName: computed('driver', 'state', function() {
const keys = this.allKeys().filter((x) => x.endsWith('Config'));
for ( let key, i = 0 ; i < keys.length ; i++ ) {
key = keys[i];
if ( get(this, key) ) {
return key;
}
}
return null;
}),
isReady: computed('conditions.@each.status', function() {
return this.hasCondition('Ready');
}),
isRKE: computed('configName', function() {
return get(this, 'configName') === 'rancherKubernetesEngineConfig';
}),
displayLocation: computed('configName', function() {
const configName = this.configName;
if ( configName ) {
return get(this, `${ configName }.region`) || get(this, `${ configName }.regionId`) || get(this, `${ configName }.location`) || get(this, `${ configName }.zone`) || get(this, `${ configName }.zoneId`);
}
return '';
}),
clusterProvider: computed('configName', 'nodePools.@each.{driver,nodeTemplateId}', 'driver', function() {
const pools = get(this, 'nodePools') || [];
const firstPool = pools.objectAt(0);
switch ( get(this, 'configName') ) {
case 'amazonElasticContainerServiceConfig':
return 'amazoneks';
case 'eksConfig':
return 'amazoneksv2';
case 'azureKubernetesServiceConfig':
return 'azureaks';
case 'googleKubernetesEngineConfig':
return 'googlegke';
case 'tencentEngineConfig':
return 'tencenttke';
case 'huaweiEngineConfig':
return 'huaweicce';
case 'okeEngineConfig':
return 'oracleoke';
case 'lkeEngineConfig':
return 'linodelke';
case 'rke2Config':
return 'rke2';
case 'rancherKubernetesEngineConfig':
if ( !pools.length ) {
return 'custom';
}
return firstPool.driver || get(firstPool, 'nodeTemplate.driver') || null;
default:
if (get(this, 'driver')) {
return get(this, 'driver');
} else {
return 'import';
}
}
}),
displayProvider: computed('configName', 'driver', 'intl.locale', 'nodePools.@each.displayProvider', 'provider', function() {
const intl = get(this, 'intl');
const pools = get(this, 'nodePools');
const firstPool = (pools || []).objectAt(0);
const configName = get(this, 'configName');
const driverName = get(this, 'driver');
switch ( configName ) {
case 'amazonElasticContainerServiceConfig':
case 'eksConfig':
return intl.t('clusterNew.amazoneks.shortLabel');
case 'azureKubernetesServiceConfig':
return intl.t('clusterNew.azureaks.shortLabel');
case 'googleKubernetesEngineConfig':
return intl.t('clusterNew.googlegke.shortLabel');
case 'tencentEngineConfig':
return intl.t('clusterNew.tencenttke.shortLabel');
case 'aliyunEngineConfig':
return intl.t('clusterNew.aliyunack.shortLabel');
case 'huaweiEngineConfig':
return intl.t('clusterNew.huaweicce.shortLabel');
case 'okeEngineConfig':
return intl.t('clusterNew.oracleoke.shortLabel');
case 'otccceEngineConfig':
return intl.t('clusterNew.otccce.shortLabel');
case 'lkeEngineConfig':
return intl.t('clusterNew.linodelke.shortLabel');
case 'k3sConfig':
return intl.t('clusterNew.k3simport.shortLabel');
case 'rke2Config':
case 'rancherKubernetesEngineConfig':
var shortLabel;
if (configName === 'rancherKubernetesEngineConfig') {
if (this.provider === 'rke.windows') {
shortLabel = 'clusterNew.rkeWindows.shortLabel'
} else {
shortLabel = 'clusterNew.rke.shortLabel'
}
} else {
shortLabel = 'clusterNew.rke2.shortLabel';
}
if ( !!pools ) {
if ( firstPool ) {
return get(firstPool, 'displayProvider') ? get(firstPool, 'displayProvider') : intl.t(shortLabel);
} else {
return intl.t(shortLabel);
}
} else {
return intl.t('clusterNew.custom.shortLabel');
}
default:
if (driverName) {
switch (driverName) {
case 'rancherd':
return intl.t('clusterNew.rancherd.shortLabel');
default:
return driverName.capitalize();
}
} else {
return intl.t('clusterNew.import.shortLabel');
}
}
}),
systemProject: computed('projects.@each.isSystemProject', function() {
let projects = (get(this, 'projects') || []).filterBy('isSystemProject', true);
return get(projects, 'firstObject');
}),
canSaveMonitor: computed('actionLinks.{editMonitoring,enableMonitoring}', 'enableClusterMonitoring', function() {
const action = get(this, 'enableClusterMonitoring') ? 'editMonitoring' : 'enableMonitoring';
return !!this.hasAction(action)
}),
canDisableMonitor: computed('actionLinks.disableMonitoring', function() {
return !!this.hasAction('disableMonitoring')
}),
defaultProject: computed('projects.@each.{name,clusterOwner}', function() {
let projects = get(this, 'projects') || [];
let out = projects.findBy('isDefault');
if ( out ) {
return out;
}
out = projects.findBy('clusterOwner', true);
if ( out ) {
return out;
}
out = projects.objectAt(0);
return out;
}),
nodeGroupVersionUpdate: computed('eksStatus.upstreamSpec.kubernetesVersion', 'eksStatus.upstreamSpec.nodeGroups.@each.version', function() {
if (isEmpty(get(this, 'eksStatus.upstreamSpec.nodeGroups'))) {
return false;
}
const kubernetesVersion = get(this, 'eksStatus.upstreamSpec.kubernetesVersion');
const nodeGroupVersions = (get(this, 'eksStatus.upstreamSpec.nodeGroups') || []).getEach('version');
return nodeGroupVersions.any((ngv) => {
if (isEmpty(ngv)) {
return false;
} else {
return Semver.lt(Semver.coerce(ngv), Semver.coerce(kubernetesVersion));
}
});
}),
certsExpiring: computed('certificatesExpiration', function() {
let { certificatesExpiration = {}, expiringCerts } = this;
if (!expiringCerts) {
expiringCerts = [];
}
if (!isEmpty(certificatesExpiration)) {
let expKeys = Object.keys(certificatesExpiration);
expKeys.forEach((kee) => {
let certDate = get(certificatesExpiration[kee], 'expirationDate');
const expirey = moment(certDate);
let diff = expirey.diff(moment());
if (diff < 2592000000) { // milliseconds in a month
expiringCerts.pushObject({
expiringCertName: kee,
milliUntil: diff,
exactDateTime: certDate
});
}
});
set(this, 'expiringCerts', expiringCerts);
return expiringCerts.length > 0;
}
return false;
}),
availableActions: computed('actionLinks.{rotateCertificates,rotateEncryptionKey}', 'canRotateEncryptionKey', 'canSaveAsTemplate', 'canShowAddHost', 'eksDisplayEksImport', 'isClusterScanDisabled', function() {
const a = get(this, 'actionLinks') || {};
return [
{
label: 'action.rotate',
icon: 'icon icon-history',
action: 'rotateCertificates',
enabled: !!a.rotateCertificates,
},
{
label: 'action.rotateEncryption',
icon: 'icon icon-key',
action: 'rotateEncryptionKey',
enabled: !!this.canRotateEncryptionKey,
// enabled: true
},
{
label: 'action.backupEtcd',
icon: 'icon icon-history',
action: 'backupEtcd',
enabled: !!a.backupEtcd,
},
{
label: 'action.restoreFromEtcdBackup',
icon: 'icon icon-history',
action: 'restoreFromEtcdBackup',
enabled: !!a.restoreFromEtcdBackup,
},
{
label: 'action.saveAsTemplate',
icon: 'icon icon-file',
action: 'saveAsTemplate',
enabled: this.canSaveAsTemplate,
},
{
label: this.eksDisplayEksImport ? 'action.importHost' : 'action.registration',
icon: 'icon icon-host',
action: 'showCommandModal',
enabled: this.canShowAddHost,
},
{
label: 'action.runCISScan',
icon: 'icon icon-play',
action: 'runCISScan',
enabled: !this.isClusterScanDisabled,
},
];
}),
isVxlan: computed('rancherKubernetesEngineConfig.network.options.flannel_backend_type', function() {
const backend = get(this, 'rancherKubernetesEngineConfig.network.options.flannel_backend_type');
return backend === 'vxlan';
}),
isWindows: computed('windowsPreferedCluster', function() {
return !!get(this, 'windowsPreferedCluster');
}),
isClusterScanDown: computed('systemProject', 'state', 'actionLinks.runSecurityScan', 'isWindows', function() {
return !get(this, 'systemProject')
|| get(this, 'state') !== 'active'
|| !get(this, 'actionLinks.runSecurityScan')
|| get(this, 'isWindows');
}),
isAddClusterScanScheduleDisabled: computed('isClusterScanDown', 'scheduledClusterScan.enabled', 'clusterTemplateRevision', 'clusterTemplateRevision.questions.[]', function() {
if (get(this, 'clusterTemplateRevision') === null) {
return get(this, 'isClusterScanDown');
}
if (get(this, 'isClusterScanDown')) {
return true;
}
if (get(this, 'scheduledClusterScan.enabled')) {
return false;
}
return !get(this, 'clusterTemplateRevision.questions')
|| get(this, 'clusterTemplateRevision.questions').every((question) => question.variable !== SCHEDULE_CLUSTER_SCAN_QUESTION_KEY)
}),
isClusterScanDisabled: computed('runningClusterScans.length', 'isClusterScanDown', function() {
return (get(this, 'runningClusterScans.length') > 0)
|| get(this, 'isClusterScanDown');
}),
unhealthyComponents: computed('componentStatuses.@each.conditions', function() {
return (get(this, 'componentStatuses') || [])
.filter((s) => !(s.conditions || []).any((c) => c.status === 'True'));
}),
masterNodes: computed('nodes.@each.{state,labels}', function() {
return (this.nodes || []).filter((node) => node.labels && node.labels[C.NODES.MASTER_NODE]);
}),
inactiveNodes: computed('nodes.@each.state', function() {
return (get(this, 'nodes') || []).filter( (n) => C.ACTIVEISH_STATES.indexOf(get(n, 'state')) === -1 );
}),
unhealthyNodes: computed('nodes.@each.conditions', function() {
const out = [];
(get(this, 'nodes') || []).forEach((n) => {
const conditions = get(n, 'conditions') || [];
const outOfDisk = conditions.find((c) => c.type === 'OutOfDisk');
const diskPressure = conditions.find((c) => c.type === 'DiskPressure');
const memoryPressure = conditions.find((c) => c.type === 'MemoryPressure');
if ( outOfDisk && get(outOfDisk, 'status') === TRUE ) {
out.push({
displayName: get(n, 'displayName'),
error: 'outOfDisk'
});
}
if ( diskPressure && get(diskPressure, 'status') === TRUE ) {
out.push({
displayName: get(n, 'displayName'),
error: 'diskPressure'
});
}
if ( memoryPressure && get(memoryPressure, 'status') === TRUE ) {
out.push({
displayName: get(n, 'displayName'),
error: 'memoryPressure'
});
}
});
return out;
}),
displayWarnings: computed('unhealthyNodes.[]', 'clusterProvider', 'inactiveNodes.[]', 'unhealthyComponents.[]', function() {
const intl = get(this, 'intl');
const out = [];
const unhealthyComponents = get(this, 'unhealthyComponents') || [];
const inactiveNodes = get(this, 'inactiveNodes') || [];
const unhealthyNodes = get(this, 'unhealthyNodes') || [];
const clusterProvider = get(this, 'clusterProvider');
const grayOut = C.GRAY_OUT_SCHEDULER_STATUS_PROVIDERS.indexOf(clusterProvider) > -1;
unhealthyComponents.forEach((component) => {
if ( grayOut && (get(component, 'name') === 'scheduler' || get(component, 'name') === 'controller-manager') ) {
return;
}
out.pushObject(intl.t('clusterDashboard.alert.component', { component: get(component, 'name') }));
});
inactiveNodes.forEach((node) => {
out.pushObject(intl.t('clusterDashboard.alert.node', { node: get(node, 'displayName') }))
});
unhealthyNodes.forEach((node) => {
out.pushObject(intl.t(`clusterDashboard.alert.nodeCondition.${ get(node, 'error') }`, { node: get(node, 'displayName') }))
});
return out;
}),
actions: {
backupEtcd() {
const getBackupType = () => {
let services = get(this, 'rancherKubernetesEngineConfig.services.etcd');
if (get(services, 'cachedConfig')) {
if (isEmpty(services.cachedConfig.s3BackupConfig)) {
return 'local';
} else if (!isEmpty(services.cachedConfig.s3BackupConfig)) {
return 's3';
}
}
}
const backupType = getBackupType();
const successTitle = this.intl.t('action.backupEtcdMessage.success.title');
const successMessage = this.intl.t('action.backupEtcdMessage.success.message', {
clusterId: this.displayName || this.id,
backupType
});
this.doAction('backupEtcd')
.then(() => this.growl.success(successTitle, successMessage))
.catch((err) => this.growl.fromError(err));
},
restoreFromEtcdBackup(options) {
get(this, 'modalService').toggleModal('modal-restore-backup', {
cluster: this,
selection: (options || {}).selection
});
},
promptDelete() {
const hasSessionToken = get(this, 'canBulkRemove') ? false : true; // canBulkRemove returns true of the session token is set false
if (hasSessionToken) {
set(this, `${ get(this, 'configName') }.accessKey`, null);
get(this, 'modalService').toggleModal('modal-delete-eks-cluster', { model: this, });
} else {
get(this, 'modalService').toggleModal('confirm-delete', {
escToClose: true,
resources: [this]
});
}
},
edit(additionalQueryParams = {}) {
let provider = get(this, 'clusterProvider') || get(this, 'driver');
let queryParams = {
queryParams: {
provider,
...additionalQueryParams
}
};
if (provider === 'amazoneks' && !isEmpty(get(this, 'eksConfig'))) {
set(queryParams, 'queryParams.provider', 'amazoneksv2');
}
if (this.clusterTemplateRevisionId) {
set(queryParams, 'queryParams.clusterTemplateRevision', this.clusterTemplateRevisionId);
}
this.router.transitionTo('authenticated.cluster.edit', get(this, 'id'), queryParams);
},
scaleDownPool(id) {
const pool = (get(this, 'nodePools') || []).findBy('id', id);
if ( pool ) {
pool.incrementQuantity(-1);
}
},
scaleUpPool(id) {
const pool = (get(this, 'nodePools') || []).findBy('id', id);
if ( pool ) {
pool.incrementQuantity(1);
}
},
saveAsTemplate() {
this.modalService.toggleModal('modal-save-rke-template', { cluster: this });
},
runCISScan(options) {
this.get('modalService').toggleModal('run-scan-modal', {
closeWithOutsideClick: true,
cluster: this,
onRun: (options || {}).onRun
});
},
rotateCertificates() {
const model = this;
get(this, 'modalService').toggleModal('modal-rotate-certificates', {
model,
serviceDefaults: get(this, 'globalStore').getById('schema', 'rotatecertificateinput').optionsFor('services'),
});
},
rotateEncryptionKey() {
const model = this;
get(this, 'modalService').toggleModal('modal-rotate-encryption-key', { model, });
},
showCommandModal() {
this.modalService.toggleModal('modal-show-command', { cluster: this });
},
},
clearConfigFieldsForClusterTemplate() {
let clearedNull = ['localClusterAuthEndpoint', 'rancherKubernetesEngineConfig', 'enableNetworkPolicy'];
let clearedDelete = ['defaultClusterRoleForProjectMembers', 'defaultPodSecurityPolicyTemplateId'];
let {
localClusterAuthEndpoint,
rancherKubernetesEngineConfig,
enableNetworkPolicy,
defaultClusterRoleForProjectMembers,
defaultPodSecurityPolicyTemplateId,
} = this;
let cachedConfig = {
localClusterAuthEndpoint,
rancherKubernetesEngineConfig,
enableNetworkPolicy,
defaultClusterRoleForProjectMembers,
defaultPodSecurityPolicyTemplateId,
};
// set this incase we fail to save the cluster;
set(this, '_cachedConfig', cachedConfig);
clearedDelete.forEach((c) => delete this[c]);
clearedNull.forEach((c) => set(this, c, null));
},
clearProvidersExcept(keep) {
const keys = this.allKeys().filter((x) => x.endsWith('Config'));
for ( let key, i = 0 ; i < keys.length ; i++ ) {
key = keys[i];
if ( key !== keep && get(this, key) ) {
set(this, key, null);
}
}
},
delete(/* arguments*/) {
const promise = this._super.apply(this, arguments);
return promise.then((/* resp */) => {
if (get(this, 'scope.currentCluster.id') === get(this, 'id')) {
get(this, 'router').transitionTo('global-admin.clusters');
}
});
},
getOrCreateToken() {
const globalStore = get(this, 'globalStore');
const id = get(this, 'id');
return globalStore.findAll('clusterRegistrationToken', { forceReload: true }).then((tokens) => {
let token = tokens.filterBy('clusterId', id)[0];
if ( token ) {
return resolve(token);
} else {
token = get(this, 'globalStore').createRecord({
type: 'clusterRegistrationToken',
clusterId: id
});
return token.save();
}
});
},
waitForClusterTemplateToBeAttached() {
return this._waitForTestFn(() => {
return this.hasClusterTemplate();
}, `Wait for Cluster Template to be attached`);
},
hasClusterTemplate() {
const { clusterTemplateId, clusterTemplateRevisionId } = this;
if (isEmpty(clusterTemplateId) && isEmpty(clusterTemplateRevisionId)) {
return false;
}
return true;
},
save(opt) {
const { globalStore, eksConfig } = this;
if (get(this, 'driver') === 'EKS' || (this.isObject(get(this, 'eksConfig')) && !this.isEmptyObject(get(this, 'eksConfig')))) {
const options = ({
...opt,
data: {
name: this.name,
eksConfig: {},
}
});
const eksClusterConfigSpec = globalStore.getById('schema', 'eksclusterconfigspec');
const nodeGroupConfigSpec = globalStore.getById('schema', 'nodegroup');
if (isEmpty(this.id)) {
sanitizeConfigs(eksClusterConfigSpec, nodeGroupConfigSpec);
if (!get(this, 'eksConfig.imported') && this.name !== get(this, 'eksConfig.displayName')) {
set(this, 'eksConfig.displayName', this.name);
}
return this._super(...arguments);
} else {
const config = jsondiffpatch.clone(get(this, 'eksConfig'));
const upstreamSpec = jsondiffpatch.clone(get(this, 'eksStatus.upstreamSpec'));
if (isEmpty(upstreamSpec)) {
sanitizeConfigs(eksClusterConfigSpec, nodeGroupConfigSpec);
return this._super(...arguments);
}
set(options, 'data.eksConfig', this.diffEksUpstream(upstreamSpec, config));
if (!isEmpty(get(options, 'data.eksConfig.nodeGroups'))) {
get(options, 'data.eksConfig.nodeGroups').forEach((ng) => {
replaceNullWithEmptyDefaults(ng, get(nodeGroupConfigSpec, 'resourceFields'));
});
}
if (get(options, 'qp._replace')) {
delete options.qp['_replace'];
}
return this._super(options);
}
} else {
return this._super(...arguments);
}
function sanitizeConfigs(eksClusterConfigSpec, nodeGroupConfigSpec) {
replaceNullWithEmptyDefaults(eksConfig, get(eksClusterConfigSpec, 'resourceFields'));
if (!isEmpty(get(eksConfig, 'nodeGroups'))) {
get(eksConfig, 'nodeGroups').forEach((ng) => {
replaceNullWithEmptyDefaults(ng, get(nodeGroupConfigSpec, 'resourceFields'));
});
}
}
function replaceNullWithEmptyDefaults(config, resourceFields) {
Object.keys(config).forEach((ck) => {
const configValue = get(config, ck);
if (configValue === null || typeof configValue === 'undefined') {
const resourceField = resourceFields[ck];
if (resourceField.type === 'string') {
set(config, ck, '');
} else if (resourceField.type.includes('array')) {
set(config, ck, []);
} else if (resourceField.type.includes('map')) {
set(config, ck, {});
} else if (resourceField.type.includes('boolean')) {
if (resourceField.default) {
set(config, ck, resourceField.default);
} else {
if ( !isEmpty(get(DEFAULT_EKS_CONFIG, ck)) || !isEmpty(get(DEFAULT_NODE_GROUP_CONFIG, ck)) ) {
let match = isEmpty(get(DEFAULT_EKS_CONFIG, ck)) ? get(DEFAULT_NODE_GROUP_CONFIG, ck) : get(DEFAULT_EKS_CONFIG, ck);
set(config, ck, match);
}
// we shouldn't get here, there are not that many fields in EKS and I've set the defaults for bools that are there
// but if we do hit this branch my some magic case imo a bool isn't something we can default cause its unknown...just dont do anything.
}
}
}
});
return config;
}
},
diffEksUpstream(lhs, rhs) {
// this is NOT a generic object diff.
// It tries to be as generic as possible but it does make certain assumptions regarding nulls and emtpy arrays/objects
// if LHS (upstream) is null and RHS (eks config) is empty we do not count this as a change
// additionally null values on the RHS will be ignored as null cant be sent in this case
const delta = {};
const rhsKeys = Object.keys(rhs);
rhsKeys.forEach((k) => {
if (k === 'type') {
return;
}
const lhsMatch = get(lhs, k);
const rhsMatch = get(rhs, k);
try {
if (isEqual(JSON.stringify(lhsMatch), JSON.stringify(rhsMatch))) {
return;
}
} catch (e){}
if (k === 'nodeGroups' || k === 'tags') {
if (!isEmpty(rhsMatch)) {
// node groups need ALL data so short circut and send it all
set(delta, k, rhsMatch);
} else {
// all node groups were deleted
set(delta, k, []);
}
return;
}
if (isEmpty(lhsMatch) || this.isEmptyObject(lhsMatch)) {
if (isEmpty(rhsMatch) || this.isEmptyObject(rhsMatch)) {
if (lhsMatch !== null && (isArray(rhsMatch) || this.isObject(rhsMatch))) {
// Empty Arrays and Empty Maps must be sent as such unless the upstream value is null, then the empty array or obj is just a init value from ember
set(delta, k, rhsMatch);
}
return;
} else {
// lhs is empty, rhs is not, just set
set(delta, k, rhsMatch);
}
} else {
if (rhsMatch !== null) {
// entry in og obj
if (isArray(lhsMatch)) {
if (isArray(rhsMatch)) {
if (!isEmpty(rhsMatch) && rhsMatch.every((m) => this.isObject(m))) {
// You have more diffing to do
rhsMatch.forEach((match) => {
// our most likely candiate for a match is node group name, but lets check the others just incase.
const matchId = get(match, 'name') || get(match, 'id') || false;
if (matchId) {
let lmatchIdx;
// we have soime kind of identifier to find a match in the upstream, so we can diff and insert to new array
const lMatch = lhsMatch.find((l, idx) => {
const lmatchId = get(l, 'name') || get(l, 'id');
if (lmatchId === matchId) {
lmatchIdx = idx;
return l;
}
});
if (lMatch) {
// we have a match in the upstream, meaning we've probably made updates to the object itself
const diffedMatch = this.diffEksUpstream(lMatch, match);
if (!isArray(get(delta, k))) {
set(delta, k, [diffedMatch]);
} else {
// diff and push into new array
delta[k].insertAt(lmatchIdx, diffedMatch);
}
} else {
// no match in upstream, new entry
if (!isArray(get(delta, k))) {
set(delta, k, [match]);
} else {
delta[k].pushObject(match);
}
}
} else {
// no match id, all we can do is dumb add
if (!isArray(get(delta, k))) {
set(delta, k, [match]);
} else {
delta[k].pushObject(match);
}
}
})
} else {
set(delta, k, rhsMatch);
}
} else {
set(delta, k, rhsMatch);
}
} else if (this.isObject(lhsMatch)) {
if (!isEmpty(rhsMatch) && !this.isEmptyObject(rhsMatch)) {
if ((Object.keys(lhsMatch) || []).length > 0) {
// You have more diffing to do
set(delta, k, this.diffEksUpstream(lhsMatch, rhsMatch));
} else if (this.isEmptyObject(lhsMatch)) {
// we had a map now we have an empty map
set(delta, k, {});
}
}
} else { // lhsMatch not an array or object
set(delta, k, rhsMatch);
}
}
}
});
return delta;
},
/**
* True if obj is a plain object, an instantiated function/class
* @param {anything} obj
*/
isObject(obj) {
return obj // Eliminates null/undefined
&& obj instanceof Object // Eliminates primitives
&& typeof obj === 'object' // Eliminates class definitions/functions
&& !Array.isArray(obj); // Eliminates arrays
},
isEmptyObject(obj) {
return this.isObject(obj) && Object.keys(obj).length === 0;
}
});