diff --git a/app/models/cluster.js b/app/models/cluster.js index 4be31619b..f0a5676bb 100644 --- a/app/models/cluster.js +++ b/app/models/cluster.js @@ -18,6 +18,40 @@ const TRUE = 'True'; const CLUSTER_TEMPLATE_ID_PREFIX = 'cattle-global-data:'; const SCHEDULE_CLUSTER_SCAN_QUESTION_KEY = 'scheduledClusterScan.enabled'; +export const DEFAULT_NODE_GROUP_CONFIG = { + desiredSize: 2, + diskSize: 20, + ec2SshKey: '', + gpu: false, + instanceType: 't3.medium', + maxSize: 2, + minSize: 2, + nodegroupName: '', + subnets: [], + type: 'nodeGroup', +}; + +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(), @@ -662,47 +696,45 @@ export default Resource.extend(Grafana, ResourceUsage, { if (get(this, 'driver') === 'EKS' || !this.isEmptyObject(get(this, 'eksConfig'))) { const options = ({ ...opt, - data: {} + data: { + name: this.name, + eksConfig: {}, + } }); - - const config = jsondiffpatch.clone(get(this, 'eksConfig')); + const eksClusterConfigSpec = get(this, 'globalStore').getById('schema', 'eksclusterconfigspec'); + const nodeGroupConfigSpec = get(this, 'globalStore').getById('schema', 'nodegroup'); if (isEmpty(this.id)) { - const eksClusterConfigSpec = get(this, 'globalStore').getById('schema', 'eksclusterconfigspec'); - const resourceFields = get(eksClusterConfigSpec, 'resourceFields'); - Object.keys(config).forEach((ck) => { - const configValue = get(config, ck); + replaceNullWithEmptyDefaults(get(this, 'eksConfig'), get(eksClusterConfigSpec, 'resourceFields')); - if (configValue === null || typeof configValue === 'undefined') { - const resourceField = resourceFields[ck]; + if (!isEmpty(get(this, 'eksConfig.nodeGroups'))) { + get(this, 'eksConfig.nodeGroups').forEach((ng) => { + replaceNullWithEmptyDefaults(ng, get(nodeGroupConfigSpec, 'resourceFields')); + }); + } - 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. - } - } - } - }) return this._super(...arguments); } else { + const config = jsondiffpatch.clone(get(this, 'eksConfig')); const upstreamSpec = jsondiffpatch.clone(get(this, 'eksStatus.upstreamSpec')); if (isEmpty(upstreamSpec)) { return reject(this.intl('clusterNew.amazoneks.errors.clusterSpec')); } - set(options, 'data', diff(upstreamSpec, config)); + 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); } @@ -710,119 +742,162 @@ export default Resource.extend(Grafana, ResourceUsage, { return this._super(...arguments); } + 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 - function diff(lhs, rhs) { - const delta = {}; - const rhsKeys = Object.keys(rhs); + const delta = {}; + const rhsKeys = Object.keys(rhs); - rhsKeys.forEach((k) => { - if (k === 'type') { + 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){} - const lhsMatch = get(lhs, k); - const rhsMatch = get(rhs, k); + if (k === 'nodeGroups') { + if (!isEmpty(lhsMatch)) { + // node groups need ALL data so short circut and send it all + set(delta, k, lhsMatch); + } else { + // all node groups were deleted + set(delta, k, []); + } - try { - if (isEqual(JSON.stringify(lhsMatch), JSON.stringify(rhsMatch))) { - return; - } - } catch (e){} + 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 + 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 { - if (rhsMatch !== null) { - // entry in og obj - if (isArray(lhsMatch)) { - if (isArray(rhsMatch)) { - if (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, 'nodegroupName') || get(match, 'name') || get(match, 'id') || false; + // 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 (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; + 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, 'nodegroupName') || get(l, 'name') || get(l, 'id'); + // 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; + if (lmatchId === matchId) { + lmatchIdx = idx; - return l; - } - }); + return l; + } + }); - if (lMatch) { - // we have a match in the upstream, meaning we've probably made updates to the object itself - const diffedMatch = diff(lMatch, match); + 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); - } + if (!isArray(get(delta, k))) { + set(delta, k, [diffedMatch]); } else { - // no match in upstream, new entry - if (!isArray(get(delta, k))) { - set(delta, k, [match]); - } else { - delta[k].pushObject(match); - } + // diff and push into new array + delta[k].insertAt(lmatchIdx, diffedMatch); } } else { - // no match id, all we can do is dumb add + // no match in upstream, new entry if (!isArray(get(delta, k))) { set(delta, k, [match]); } else { delta[k].pushObject(match); } } - }) - } else { - set(delta, k, rhsMatch); - } + } 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 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, diff(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 + } 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; - } + return delta; }, - isObject(obj) { return obj !== null && obj.constructor.name === 'Object'; }, diff --git a/lib/shared/addon/components/cluster-driver/driver-eks/component.js b/lib/shared/addon/components/cluster-driver/driver-eks/component.js index b2c60eff3..a226d2d3f 100644 --- a/lib/shared/addon/components/cluster-driver/driver-eks/component.js +++ b/lib/shared/addon/components/cluster-driver/driver-eks/component.js @@ -14,39 +14,7 @@ import ClusterDriver from 'shared/mixins/cluster-driver'; import { EKS_REGIONS, EKS_VERSIONS, INSTANCE_TYPES, nameFromResource } from 'shared/utils/amazon'; import { randomStr } from 'shared/utils/util'; import layout from './template'; - -const DEFAULT_NODE_GROUP_CONFIG = { - desiredSize: 2, - diskSize: 20, - ec2SshKey: '', - gpu: false, - instanceType: 't3.medium', - maxSize: 2, - minSize: 2, - nodegroupName: '', - subnets: [], - type: 'nodeGroup', -}; - -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', -}; +import { DEFAULT_NODE_GROUP_CONFIG, DEFAULT_EKS_CONFIG } from 'ui/models/cluster'; export default Component.extend(ClusterDriver, { intl: service(),