ui/app/models/cluster.js

1513 lines
50 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: '',
nodeRole: '',
requestSpotInstances: false,
resourceTags: {},
spotInstanceTypes: [],
subnets: [],
tags: {},
type: 'nodeGroup',
userData: DEFAULT_USER_DATA,
};
export const DEFAULT_EKS_CONFIG = {
amazonCredentialSecret: '',
displayName: '',
ebsCSIDriver: false,
imported: false,
kmsKey: '',
kubernetesVersion: '',
loggingTypes: [],
nodeGroups: [],
privateAccess: false,
publicAccess: true,
publicAccessSources: [],
region: 'us-west-2',
secretsEncryption: false,
securityGroups: [],
serviceRole: '',
subnets: [],
tags: {},
type: 'eksclusterconfigspec',
};
export const DEFAULT_GKE_NODE_POOL_CONFIG = {
autoscaling: {
enabled: false,
maxNodeCount: null,
minNodeCount: null,
},
config: {
diskSizeGb: 100,
diskType: 'pd-standard',
imageType: 'COS_CONTAINERD',
labels: null,
localSsdCount: 0,
machineType: 'n1-standard-2',
oauthScopes: null,
preemptible: false,
taints: null,
tags: null,
},
initialNodeCount: 3,
management: {
autoRepair: true,
autoUpgrade: true,
},
maxPodsConstraint: 110,
name: null,
version: null,
type: 'gkenodepoolconfig'
};
export const DEFAULT_GKE_CONFIG = {
clusterAddons: {
horizontalPodAutoscaling: true,
httpLoadBalancing: true,
networkPolicyConfig: false
},
clusterIpv4Cidr: '',
clusterName: null,
description: null,
enableKubernetesAlpha: false,
googleCredentialSecret: null,
imported: false,
ipAllocationPolicy: {
clusterIpv4CidrBlock: null,
clusterSecondaryRangeName: null,
createSubnetwork: false,
nodeIpv4CidrBlock: null,
servicesIpv4CidrBlock: null,
servicesSecondaryRangeName: null,
subnetworkName: null,
useIpAliases: true
},
kubernetesVersion: '',
labels: {},
locations: null,
loggingService: 'logging.googleapis.com/kubernetes',
maintenanceWindow: '',
masterAuthorizedNetworks: {
cidrBlocks: null,
enabled: false
},
monitoringService: 'monitoring.googleapis.com/kubernetes',
network: null,
networkPolicyEnabled: false,
nodePools: [DEFAULT_GKE_NODE_POOL_CONFIG],
privateClusterConfig: {
enablePrivateEndpoint: false,
enablePrivateNodes: false,
masterIpv4CidrBlock: null,
},
projectID: null,
region: null,
subnetwork: null,
type: 'gkeclusterconfigspec',
zone: 'us-central1-c',
};
export const DEFAULT_AKS_CONFIG = {
authBaseUrl: null,
authorizedIpRanges: [],
azureCredentialSecret: null,
baseUrl: null,
clusterName: null,
dnsPrefix: null,
dnsServiceIp: null,
dockerBridgeCidr: null,
imported: false,
kubernetesVersion: null,
linuxAdminUsername: 'azureuser',
loadBalancerSku: 'Standard',
networkPlugin: 'kubenet',
networkPolicy: null,
nodePools: [],
podCidr: null,
privateCluster: false,
resourceGroup: null,
resourceLocation: 'eastus',
serviceCidr: null,
sshPublicKey: null,
subnet: null,
tags: {},
type: 'aksclusterconfigspec',
virtualNetwork: null,
virtualNetworkResourceGroup: null,
windowsAdminPassword: null,
windowsAdminUsername: null,
};
export const DEFAULT_AKS_NODE_POOL_CONFIG = {
availabilityZones: ['1', '2', '3'],
count: 1,
enableAutoScaling: false,
maxPods: 110,
maxSurge: '1',
mode: 'System',
name: '',
nodeLabels: {},
nodeTaints: [],
orchestratorVersion: '',
osDiskSizeGB: 128,
osDiskType: 'Managed',
osType: 'Linux',
type: 'aksnodepool',
vmSize: 'Standard_DS2_v2',
}
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,
canHaveLabels: true,
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),
isRKE: computed.equal('configName', 'rancherKubernetesEngineConfig'),
conditionsDidChange: on('init', observer('enableClusterMonitoring', 'conditions.@each.status', function() {
if ( !this.enableClusterMonitoring ) {
return false;
}
const conditions = this.conditions || [];
const ready = conditions.findBy('type', 'MonitoringEnabled');
const status = ready && get(ready, 'status') === 'True';
if ( status !== this.isMonitoringReady ) {
set(this, 'isMonitoringReady', status);
}
})),
clusterTemplateDisplayName: computed('clusterTemplate.{displayName,name}', 'clusterTemplateId', function() {
const displayName = get(this, 'clusterTemplate.displayName');
const clusterTemplateId = (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 = (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 this.canBulkRemove ? 'delete' : null;
}),
hasSessionToken: computed('annotations', function() {
const sessionTokenLabel = `${ (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 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;
}),
hasPublicAccess: computed('aksConfig.privateCluster', 'aksStatus.upstreamSpec.privateCluster', 'eksConfig.publicAccess', 'eksStatus.upstreamSpec.publicAccess', 'gkeStatus.privateClusterConfig.enablePrivateNodes', 'gkeStatus.upstreamSpec.privateClusterConfig.enablePrivateNodes', function() {
const { clusterProvider } = this;
switch (clusterProvider) {
case 'amazoneksv2':
return this?.eksStatus?.upstreamSpec?.publicAccess || this?.eksConfig?.publicAccess || true;
case 'googlegkev2':
return !this?.gkeStatus?.upstreamSpec?.privateClusterConfig?.enablePrivateNodes || !this?.gkeStatus?.privateClusterConfig?.enablePrivateNodes || true;
case 'azureaksv2':
return !this?.aksStatus?.upstreamSpec?.privateCluster || !this?.aksConfig?.privateCluster || true;
default:
return true;
}
}),
hasPrivateAccess: computed('aksConfig.privateCluster', 'aksStatus.upstreamSpec.privateCluster', 'eksConfig.privateAccess', 'eksStatus.upstreamSpec.privateAccess', 'gkeConfig.privateClusterConfig.enablePrivateNodes', 'gkeStatus.upstreamSpec.privateClusterConfig.enablePrivateNodes', function() {
const { clusterProvider } = this;
switch (clusterProvider) {
case 'amazoneksv2':
return this?.eksStatus?.upstreamSpec?.privateAccess || this?.eksConfig?.privateAccess || false;
case 'googlegkev2':
return this?.gkeStatus?.upstreamSpec?.privateClusterConfig?.enablePrivateNodes || this?.gkeConfig?.privateClusterConfig?.enablePrivateNodes;
case 'azureaksv2':
return this?.aksStatus?.upstreamSpec?.privateCluster || this?.aksConfig?.privateCluster;
default:
return false;
}
}),
displayImportLabel: computed('aksDisplayImport', 'clusterProvider', 'eksDisplayEksImport', 'gkeDisplayImport', function() {
const { clusterProvider } = this;
switch (clusterProvider) {
case 'amazoneksv2':
return this.eksDisplayEksImport ? true : false;
case 'googlegkev2':
return this.gkeDisplayImport ? true : false;
case 'azureaksv2':
return this.aksDisplayImport ? true : false;
case 'import':
return true;
default:
return false;
}
}),
aksDisplayImport: computed('clusterProvider', 'hasPrivateAccess', 'imported', function() {
const { clusterProvider } = this;
if (clusterProvider !== 'azureaksv2') {
return false;
}
if (this.hasPrivateAccess) {
return true;
}
return false;
}),
gkeDisplayImport: computed('clusterProvider', 'hasPrivateAccess', 'imported', function() {
const { clusterProvider } = this;
if (clusterProvider !== 'googlegkev2') {
return false;
}
if (this.hasPrivateAccess) {
return true;
}
return false;
}),
eksDisplayEksImport: computed('hasPrivateAccess', 'hasPublicAccess', function() {
const { clusterProvider } = this;
if (clusterProvider !== 'amazoneksv2') {
return false;
}
if (!this.hasPublicAccess && this.hasPrivateAccess) {
return true;
}
return false;
}),
canShowAddHost: computed('clusterProvider', 'hasPrivateAccess', 'hasPublicAccess', 'imported', 'nodes', 'internal', function() {
const { clusterProvider } = this;
const compatibleProviders = ['custom', 'import', 'amazoneksv2', 'googlegkev2', 'azureaksv2'];
// internal indicates the local cluster. Rancher does not manage the local cluster, so nodes can not be added via the UI
const internal = this.internal;
if (!compatibleProviders.includes(clusterProvider) || internal) {
return false;
}
// private access requires the ability to run the import command on the cluster
if (clusterProvider === 'amazoneksv2' && !!this.hasPublicAccess && this.hasPrivateAccess) {
return true;
} else if (clusterProvider === 'googlegkev2' && this.hasPrivateAccess) {
return true;
} else if (clusterProvider === 'azureaksv2' && this.hasPrivateAccess) {
return true;
} else if (( clusterProvider === 'custom' || clusterProvider === 'import')) {
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');
}),
isK8s21Plus: computed('version.gitVersion', function() {
const version = Semver.coerce(get(this, 'version.gitVersion'));
return Semver.satisfies(version, '>=1.21.0');
}),
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 = this.nodePools || [];
const firstPool = pools.objectAt(0);
switch ( this.configName ) {
case 'amazonElasticContainerServiceConfig':
return 'amazoneks';
case 'eksConfig':
return 'amazoneksv2';
case 'azureKubernetesServiceConfig':
return 'azureaks';
case 'aksConfig':
return 'azureaksv2';
case 'gkeConfig':
return 'googlegkev2';
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 (this.driver && this.configName && !isEmpty(get(this, this.configName))) {
return this.driver;
} else {
return 'import';
}
}
}),
displayProvider: computed('configName', 'driver', 'intl.locale', 'nodePools.@each.displayProvider', 'provider', function() {
const intl = this.intl;
const pools = this.nodePools;
const firstPool = (pools || []).objectAt(0);
const configName = this.configName;
const driverName = 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 = (this.projects || []).filterBy('isSystemProject', true);
return get(projects, 'firstObject');
}),
canSaveMonitor: computed('actionLinks.{editMonitoring,enableMonitoring}', 'enableClusterMonitoring', function() {
const action = 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 = 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));
}
});
}),
gkeNodePoolVersionUpdate: computed('gkeStatus.upstreamSpec.kubernetesVersion', 'gkeStatus.upstreamSpec.nodePools.@each.version', function() {
if (isEmpty(get(this, 'gkeStatus.upstreamSpec.nodePools'))) {
return false;
}
const kubernetesVersion = get(this, 'gkeStatus.upstreamSpec.kubernetesVersion');
const nodePoolVersions = (get(this, 'gkeStatus.upstreamSpec.nodePools') || []).getEach('version');
return nodePoolVersions.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', 'displayImportLabel', 'isClusterScanDisabled', function() {
const a = 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.displayImportLabel ? '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 !!this.windowsPreferedCluster;
}),
isClusterScanDown: computed('systemProject', 'state', 'actionLinks.runSecurityScan', 'isWindows', function() {
return !this.systemProject
|| this.state !== 'active'
|| !get(this, 'actionLinks.runSecurityScan')
|| this.isWindows;
}),
isAddClusterScanScheduleDisabled: computed('isClusterScanDown', 'scheduledClusterScan.enabled', 'clusterTemplateRevision', 'clusterTemplateRevision.questions.[]', function() {
if (this.clusterTemplateRevision === null) {
return this.isClusterScanDown;
}
if (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)
|| this.isClusterScanDown;
}),
unhealthyComponents: computed('componentStatuses.@each.conditions', function() {
return (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 (this.nodes || []).filter( (n) => C.ACTIVEISH_STATES.indexOf(get(n, 'state')) === -1 );
}),
unhealthyNodes: computed('nodes.@each.conditions', function() {
const out = [];
(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 = this.intl;
const out = [];
const unhealthyComponents = this.unhealthyComponents || [];
const inactiveNodes = this.inactiveNodes || [];
const unhealthyNodes = this.unhealthyNodes || [];
const clusterProvider = 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) {
this.modalService.toggleModal('modal-restore-backup', {
cluster: this,
selection: (options || {}).selection
});
},
promptDelete() {
const hasSessionToken = this.canBulkRemove ? false : true; // canBulkRemove returns true of the session token is set false
if (hasSessionToken) {
set(this, `${ this.configName }.accessKey`, null);
this.modalService.toggleModal('modal-delete-eks-cluster', { model: this, });
} else {
this.modalService.toggleModal('confirm-delete', {
escToClose: true,
resources: [this]
});
}
},
edit(additionalQueryParams = {}) {
let provider = this.clusterProvider || this.driver;
let queryParams = {
queryParams: {
provider,
...additionalQueryParams
}
};
if (provider === 'import' &&
isEmpty(this.eksConfig) &&
isEmpty(this.gkeConfig) &&
isEmpty(this.aksConfig)) {
set(queryParams, 'queryParams.importProvider', 'other');
}
if (provider === 'amazoneks' && !isEmpty(this.eksConfig)) {
set(queryParams, 'queryParams.provider', 'amazoneksv2');
}
if (provider === 'gke' && !isEmpty(this.gkeConfig)) {
set(queryParams, 'queryParams.provider', 'googlegkev2');
}
if (provider === 'aks' && !isEmpty(this.aksConfig)) {
set(queryParams, 'queryParams.provider', 'azureaksv2');
}
if (this.clusterTemplateRevisionId) {
set(queryParams, 'queryParams.clusterTemplateRevision', this.clusterTemplateRevisionId);
}
this.router.transitionTo('authenticated.cluster.edit', this.id, queryParams);
},
scaleDownPool(id) {
const pool = (this.nodePools || []).findBy('id', id);
if ( pool ) {
pool.incrementQuantity(-1);
}
},
scaleUpPool(id) {
const pool = (this.nodePools || []).findBy('id', id);
if ( pool ) {
pool.incrementQuantity(1);
}
},
saveAsTemplate() {
this.modalService.toggleModal('modal-save-rke-template', { cluster: this });
},
runCISScan(options) {
this.modalService.toggleModal('run-scan-modal', {
closeWithOutsideClick: true,
cluster: this,
onRun: (options || {}).onRun
});
},
rotateCertificates() {
const model = this;
this.modalService.toggleModal('modal-rotate-certificates', {
model,
serviceDefaults: this.globalStore.getById('schema', 'rotatecertificateinput').optionsFor('services'),
});
},
rotateEncryptionKey() {
const model = this;
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'];
let {
localClusterAuthEndpoint,
rancherKubernetesEngineConfig,
enableNetworkPolicy,
defaultClusterRoleForProjectMembers,
} = this;
let cachedConfig = {
localClusterAuthEndpoint,
rancherKubernetesEngineConfig,
enableNetworkPolicy,
defaultClusterRoleForProjectMembers,
};
// 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') === this.id) {
this.router.transitionTo('global-admin.clusters');
}
});
},
getOrCreateToken() {
const globalStore = this.globalStore;
const id = this.id;
return globalStore.findAll('clusterRegistrationToken', { forceReload: true }).then((tokens) => {
let token = tokens.filterBy('clusterId', id)[0];
if ( token ) {
return resolve(token);
} else {
token = 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;
},
compareStringArrays(a, b) {
let aStr = '';
let bStr = '';
Object.keys(a || {}).sort().forEach((key) => {
aStr += `${ key }=${ a[key] },`;
});
Object.keys(b || {}).sort().forEach((key) => {
bStr += `${ key }=${ b[key] },`;
});
return aStr !== bStr;
},
save(opt, originalModel) {
const {
eksConfig, gkeConfig, aksConfig
} = this;
let options = null;
if (this.driver === 'EKS' || (this.isObject(eksConfig) && !this.isEmptyObject(eksConfig))) {
options = this.syncEksConfigs(opt);
} else if (this.isObject(gkeConfig) && !this.isEmptyObject(gkeConfig)) {
options = this.syncGkeConfigs(opt);
} else if (this.isObject(aksConfig) && !this.isEmptyObject(aksConfig)) {
options = this.syncAksConfigs(opt);
}
if (!isEmpty(options)) {
if (originalModel && originalModel.model && originalModel.model.originalCluster) {
// Check to see if the labels have changed and send them, if they have
if (this.compareStringArrays(originalModel.model.originalCluster.labels, this.labels)) {
options.data.labels = this.labels;
}
// Check to see if the annotations have changed and send them, if they have
if (this.compareStringArrays(originalModel.model.originalCluster.annotations, this.annotations)) {
options.data.annotations = this.annotations;
}
const { clusterAgentDeploymentCustomization = {}, fleetAgentDeploymentCustomization = {} } = originalModel.model.originalCluster
const { clusterAgentDeploymentCustomization:newClusterAgentDeploymentCustomization = {}, fleetAgentDeploymentCustomization: newFleetAgentDeploymentCustomization = {} } = this;
if (JSON.stringify(clusterAgentDeploymentCustomization) !== JSON.stringify(newClusterAgentDeploymentCustomization)){
options.data.clusterAgentDeploymentCustomization = this.addDeletedKeysAsNull(clusterAgentDeploymentCustomization, newClusterAgentDeploymentCustomization)
}
if (JSON.stringify(fleetAgentDeploymentCustomization) !== JSON.stringify(newFleetAgentDeploymentCustomization)){
options.data.fleetAgentDeploymentCustomization = this.addDeletedKeysAsNull(fleetAgentDeploymentCustomization, newFleetAgentDeploymentCustomization)
}
}
return this._super(options);
}
return this._super(...arguments);
},
/**
* When editing EKS, GKE, AKS imported clusters, only properties that have changed are sent in the request.
* This is part of a strategy to avoid overwriting properties that have changed in the aws/google cloud/azure console
* Unfortunately this creates issues when editing agent config customizations: when the user clears a previously-defined field the cluster save request omits that key and the previous value is preserved
* Sending null instead of removing the key properly overwrites the old value
* We're adding null here instead of changing the key-removing functionality in agent config components to avoid poluting RKE1 'edit as yaml' view & save request
*/
addDeletedKeysAsNull(original, toSave = {}){
Object.keys(original).forEach((key) => {
if (!toSave[key]){
toSave[key] = null
} else if (original[key] && typeof original[key] === 'object' && !Array.isArray(original[key])){
set(toSave, key, this.addDeletedKeysAsNull(original[key], toSave[key]))
}
})
return toSave
},
syncAksConfigs(opt) {
const {
aksConfig, globalStore, id
} = this;
const options = ({
...opt,
data: {
name: this.name,
aksConfig: {},
}
});
const aksClusterConfigSpec = globalStore.getById('schema', 'aksclusterconfigspec');
const aksNodePoolConfigSpec = globalStore.getById('schema', 'aksnodepool');
if (isEmpty(id)) {
this.sanitizeConfigs(aksConfig, aksClusterConfigSpec, aksNodePoolConfigSpec, 'nodePools');
if (!get(this, 'aksConfig.imported') && this.name !== get(this, 'aksConfig.clusterName')) {
set(this, 'aksConfig.clusterName', this.name);
}
return;
} else {
const config = jsondiffpatch.clone(aksConfig);
const upstreamSpec = jsondiffpatch.clone(get(this, 'aksStatus.upstreamSpec'));
if (isEmpty(upstreamSpec)) {
this.sanitizeConfigs(aksConfig, aksClusterConfigSpec, aksNodePoolConfigSpec, 'nodePools');
return;
}
set(options, 'data.aksConfig', this.diffUpstreamSpec(upstreamSpec, config));
if (!isEmpty(get(options, 'data.aksConfig.nodePools'))) {
get(options, 'data.aksConfig.nodePools').forEach((np) => {
this.replaceNullWithEmptyDefaults(np, get(aksNodePoolConfigSpec, 'resourceFields'));
});
}
if (get(options, 'qp._replace')) {
delete options.qp['_replace'];
}
return options;
}
},
syncGkeConfigs(opt) {
const {
gkeConfig, globalStore, id
} = this;
const options = ({
...opt,
data: {
name: this.name,
gkeConfig: {},
}
});
const gkeClusterConfigSpec = globalStore.getById('schema', 'gkeclusterconfigspec');
const gkeNodePoolConfigSpec = globalStore.getById('schema', 'gkenodepoolconfig'); // will be gkeNodeConfig
if (isEmpty(id)) {
this.sanitizeConfigs(gkeConfig, gkeClusterConfigSpec, gkeNodePoolConfigSpec, 'nodePools');
if (!get(this, 'gkeConfig.imported') && this.name !== get(this, 'gkeConfig.clusterName')) {
set(this, 'gkeConfig.clusterName', this.name);
}
return;
} else {
const config = jsondiffpatch.clone(gkeConfig);
const upstreamSpec = jsondiffpatch.clone(get(this, 'gkeStatus.upstreamSpec'));
if (isEmpty(upstreamSpec)) {
this.sanitizeConfigs(gkeConfig, gkeClusterConfigSpec, gkeNodePoolConfigSpec, 'nodePools');
return;
}
set(options, 'data.gkeConfig', this.diffUpstreamSpec(upstreamSpec, config));
if (!isEmpty(get(options, 'data.gkeConfig.nodePools'))) {
get(options, 'data.gkeConfig.nodePools').forEach((ng) => {
this.replaceNullWithEmptyDefaults(ng, get(gkeNodePoolConfigSpec, 'resourceFields'));
});
}
if (get(options, 'qp._replace')) {
delete options.qp['_replace'];
}
return options;
}
},
syncEksConfigs(opt) {
const { eksConfig, globalStore, } = this;
const options = ({
...opt,
data: {
name: this.name,
eksConfig: {},
}
});
const eksClusterConfigSpec = globalStore.getById('schema', 'eksclusterconfigspec');
const nodeGroupConfigSpec = globalStore.getById('schema', 'nodegroup');
if (isEmpty(this.id)) {
this.sanitizeConfigs(eksConfig, eksClusterConfigSpec, nodeGroupConfigSpec);
if (!get(this, 'eksConfig.imported') && this.name !== get(this, 'eksConfig.displayName')) {
set(this, 'eksConfig.displayName', this.name);
}
return;
} else {
const config = jsondiffpatch.clone(this.eksConfig);
const upstreamSpec = jsondiffpatch.clone(get(this, 'eksStatus.upstreamSpec'));
if (isEmpty(upstreamSpec)) {
this.sanitizeConfigs(eksConfig, eksClusterConfigSpec, nodeGroupConfigSpec);
return;
}
set(options, 'data.eksConfig', this.diffUpstreamSpec(upstreamSpec, config));
if (!isEmpty(get(options, 'data.eksConfig.nodeGroups'))) {
get(options, 'data.eksConfig.nodeGroups').forEach((ng) => {
this.replaceNullWithEmptyDefaults(ng, get(nodeGroupConfigSpec, 'resourceFields'));
});
}
if (get(options, 'qp._replace')) {
delete options.qp['_replace'];
}
return options;
}
},
sanitizeConfigs(currentConfig, clusterConfigSpec, nodeGroupConfigSpec, nodeType = 'nodeGroups') {
this.replaceNullWithEmptyDefaults(currentConfig, get(clusterConfigSpec, 'resourceFields'));
if (!isEmpty(get(currentConfig, nodeType))) {
get(currentConfig, nodeType).forEach((ng) => {
this.replaceNullWithEmptyDefaults(ng, get(nodeGroupConfigSpec, 'resourceFields'));
});
}
},
replaceNullWithEmptyDefaults(config, resourceFields) {
const { clusterProvider } = this;
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 {
// 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.
if (clusterProvider === 'amazoneksv2') {
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);
}
} else if (clusterProvider === 'googlegkev2') {
if ( !isEmpty(get(DEFAULT_GKE_CONFIG, ck)) || !isEmpty(get(DEFAULT_GKE_NODE_POOL_CONFIG, ck)) ) {
let match = isEmpty(get(DEFAULT_GKE_CONFIG, ck)) ? get(DEFAULT_GKE_NODE_POOL_CONFIG, ck) : get(DEFAULT_GKE_CONFIG, ck);
set(config, ck, match);
}
} else if (clusterProvider === 'azureaksv2') {
if ( !isEmpty(get(DEFAULT_AKS_CONFIG, ck)) || !isEmpty(get(DEFAULT_AKS_NODE_POOL_CONFIG, ck)) ) {
let match = isEmpty(get(DEFAULT_AKS_CONFIG, ck)) ? get(DEFAULT_AKS_NODE_POOL_CONFIG, ck) : get(DEFAULT_AKS_CONFIG, ck);
set(config, ck, match);
}
}
}
}
}
});
},
diffUpstreamSpec(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);
if (k !== 'nodeGroups' && k !== 'nodePools') {
try {
if (isEqual(JSON.stringify(lhsMatch), JSON.stringify(rhsMatch))) {
return;
}
} catch (e){}
}
if (k === 'nodeGroups' || k === 'nodePools' || k === 'tags' || k === 'labels') {
// Node Groups and Node Pools do not require a sync, we can safely send the entire object
// Tags and Labels (maps) are also included by default because what is present in the config is exactly what should be used on save and any equal maps would have been caught by the JSON isEqual comparison above
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.diffUpstreamSpec(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.diffUpstreamSpec(lhsMatch, rhsMatch));
} else if (this.isEmptyObject(lhsMatch)) {
// we had a map now we have an empty map
set(delta, k, {});
}
} else if (!this.isEmptyObject(lhsMatch) && this.isEmptyObject(rhsMatch)) {
// 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;
}
});