ui/lib/nodes/addon/components/driver-harvester/component.js

801 lines
24 KiB
JavaScript

import { alias } from '@ember/object/computed';
import {
get, set, computed, observer, setProperties
} from '@ember/object';
import Component from '@ember/component';
import NodeDriver from 'shared/mixins/node-driver';
import layout from './template';
import { inject as service } from '@ember/service';
import { throttledObserver } from 'ui/utils/debounce';
import { hash } from 'rsvp';
const DRIVER = 'harvester';
const CONFIG = 'harvesterConfig';
const SYSTEM_NAMESPACES = [
'cattle-dashboards',
'cattle-global-data',
'cattle-system',
'gatekeeper-system',
'ingress-nginx',
'kube-node-lease',
'kube-public',
'kube-system',
'linkerd',
'rio-system',
'security-scan',
'tekton-pipelines',
];
const TYPE = {
AFFINITY: 'affinity',
ANTI_AFFINITY: 'antiAffinity'
};
const PRIORITY = {
REQUIRED: 'required',
PREFERRED: 'preferred'
};
const STORAGE_NETWORK = 'storage-network.settings.harvesterhci.io'
export default Component.extend(NodeDriver, {
growl: service(),
settings: service(),
intl: service(),
layout,
driverName: DRIVER,
model: {},
currentCluster: null,
clusters: [],
clusterContent: [],
imageContent: [],
networkContent: [],
namespaceContent: [],
nodes: [],
namespaces: [],
nodeSchedulings: [],
podSchedulings: [],
networkDataContent: [],
storageClassContent: [],
defaultStorageClass: '',
userDataContent: [],
controller: null,
signal: '',
isImportMode: true,
loading: false,
disks: [],
interfaces: [],
config: alias(`model.${ CONFIG }`),
init() {
this._super(...arguments);
const controller = new AbortController();
set(this, 'controller', controller);
this.fetchResource();
if (!!get(this, 'config.vmAffinity')) {
this.initSchedulings();
}
this.initDisks()
this.initInterfaces()
},
actions: {
async finishAndSelectCloudCredential(credential) {
await this.globalStore.findAll('cloudcredential', { forceReload: true });
set(this, 'model.cloudCredentialId', get(credential, 'id'));
},
updateYaml(type, value) {
set(this, `config.${ type }`, value);
},
addNodeScheduling() {
const neu = {
priority: PRIORITY.REQUIRED,
nodeSelectorTerms: { matchExpressions: [] },
};
this.get('nodeSchedulings').pushObject(neu);
},
addVolume(type) {
let neu = {}
if (type === 'volume') {
neu = {
storageClassName: get(this, 'defaultStorageClass'),
size: 10,
bootOrder: 0
};
} else if (type === 'image') {
neu = {
imageName: '',
size: 40,
bootOrder: 0
};
}
this.get('disks').pushObject(neu);
},
addNetwork() {
const neu = {
networkName: '',
macAddress: ''
}
this.get('interfaces').pushObject(neu);
},
removeNodeScheduling(scheduling) {
this.get('nodeSchedulings').removeObject(scheduling);
},
removeDisk(disk) {
this.get('disks').removeObject(disk);
},
removeNetwork(network) {
this.get('interfaces').removeObject(network);
},
updateNodeScheduling() {
this.parseNodeScheduling();
},
addPodScheduling() {
const neu = {
type: TYPE.AFFINITY,
priority: PRIORITY.REQUIRED,
labelSelector: { matchExpressions: [] },
topologyKey: ''
};
this.get('podSchedulings').pushObject(neu);
},
removePodScheduling(scheduling) {
this.get('podSchedulings').removeObject(scheduling);
},
updatePodScheduling() {
this.parsePodScheduling();
},
},
clearData: observer('currentCredential.id', function() {
set(this, 'config.imageName', '');
set(this, 'config.networkName', '');
set(this, 'config.vmNamespace', '');
set(this, 'nodeSchedulings', []);
set(this, 'podSchedulings', []);
set(this, 'vmAffinity', {});
set(this, 'config.vmAffinity', '');
set(this, 'config.diskInfo', '');
set(this, 'config.networkInfo', '');
this.initDisks()
this.initInterfaces()
}),
setDiskInfo: observer('disks.@each.{imageName,bootOrder,storageClassName,size}', function() {
const diskInfo = {
disks: get(this, 'disks').map((disk) => {
return {
...disk,
size: Number(disk.size),
};
})
};
set(this, 'config.diskInfo', JSON.stringify(diskInfo));
}),
setNetworkInfo: observer('interfaces.@each.{networkName,macAddress}', function() {
const networkInfo = { interfaces: get(this, 'interfaces') };
set(this, 'config.networkInfo', JSON.stringify(networkInfo))
}),
nodeSchedulingsChanged: observer('nodeSchedulings.[]', function() {
this.parseNodeScheduling();
}),
podSchedulingsChanged: observer('podSchedulings.[]', function() {
this.parsePodScheduling();
}),
fetchResource: throttledObserver('currentCredential.id', 'currentCredential.harvestercredentialConfig.clusterId', async function() {
const clusterId = get(this, 'currentCredential') && get(this, 'currentCredential').harvestercredentialConfig && get(this, 'currentCredential').harvestercredentialConfig.clusterId;
const url = clusterId === 'local' ? '' : `/k8s/clusters/${ clusterId }`;
if (!clusterId) {
return;
}
let controller = get(this, 'controller');
let signal = get(this, 'signal');
signal = controller.signal;
set(this, 'signal', signal);
set(this, 'loading', true);
hash({ nodes: get(this, 'globalStore').rawRequest({ url: `${ url }/v1/node` }) }).then((resp) => {
set(this, 'nodes', resp.nodes.body.data || []);
}).catch((err) => {
const message = err.statusText || err.message;
set(this, 'nodes', []);
get(this, 'growl').fromError('Error request Node API', message);
})
hash({
images: get(this, 'globalStore').rawRequest({ url: `${ url }/v1/harvesterhci.io.virtualmachineimages` }),
networks: get(this, 'globalStore').rawRequest({ url: `${ url }/v1/k8s.cni.cncf.io.networkattachmentdefinition` }),
namespaces: get(this, 'globalStore').rawRequest({ url: `${ url }/v1/namespace` }),
configmaps: get(this, 'globalStore').rawRequest({ url: `${ url }/v1/configmap` }),
storageClass: get(this, 'globalStore').rawRequest({ url: `${ url }/v1/storage.k8s.io.storageclasses` }),
systemNamespace: get(this, 'globalStore').rawRequest({ url: `${ url }/v1/management.cattle.io.settings/system-namespaces` }),
}).then((resp) => {
const images = resp.images.body.data || [];
const imageContent = images.filter((O) => {
return !O.spec.url.endsWith('.iso') && this.isReady.call(O);
}).map((O) => {
const value = O.id;
const label = `${ O.spec.displayName } (${ value })`;
return {
label,
value
}
});
const networks = resp.networks.body.data || [];
const networkContent = networks.filter((O) => {
return O.metadata?.annotations?.[STORAGE_NETWORK] !== 'true'
}).map((O) => {
let id = '';
try {
const config = JSON.parse(O.spec.config);
id = config.vlan;
} catch (err) {
get(this, 'growl').fromError('Error parse network config', err);
}
const value = O.id;
const label = `${ value } (vlanId=${ id })`;
return {
label,
value
}
});
const systemNamespaceValue = resp.systemNamespace.body.value || '';
const systemNamespaces = systemNamespaceValue.split(',');
const namespaces = resp.namespaces.body.data || [];
const namespaceContent = namespaces
.filter((O) => {
return !this.isSystemNamespace(O) && O.links.update && !systemNamespaces.includes(O.metadata.name);
})
.map((O) => {
const value = O.id;
const label = O.id;
return {
label,
value
}
});
const configmaps = resp.configmaps.body.data || [];
const networkDataContent = [];
const userDataContent = [];
configmaps.map((O) => {
const cloudTemplate = O.metadata && O.metadata.labels && O.metadata.labels['harvesterhci.io/cloud-init-template'];
const value = O.data && O.data.cloudInit;
const label = O.metadata.name;
if (cloudTemplate === 'user') {
userDataContent.push({
label,
value
})
} else if (cloudTemplate === 'network') {
networkDataContent.push({
label,
value
})
}
})
const storageClass = resp.storageClass.body.data || [];
let defaultStorageClass = '';
const storageClassContent = storageClass.filter((s) => !s.parameters?.backingImage).map((s) => {
const isDefault = s.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true';
const label = isDefault ? `${ s.metadata.name } (${ this.intl.t('generic.default') })` : s.metadata.name;
if (isDefault) {
defaultStorageClass = s.metadata.name;
}
return {
label,
value: s.metadata.name,
};
}) || [];
setProperties(this, {
imageContent,
networkContent,
namespaceContent,
userDataContent,
networkDataContent,
storageClassContent,
defaultStorageClass
});
}).catch((err) => {
setProperties(this, {
imageContent: [],
networkContent: [],
namespaceContent: [],
userDataContent: [],
networkDataContent: [],
namespaces: [],
vmAffinity: [],
nodeSchedulings: [],
podSchedulings: [],
storageClassContent: [],
})
const message = err.statusText || err.message;
get(this, 'growl').fromError('Error request Image API', message);
}).finally(() => {
set(this, 'loading', false);
})
controller.abort()
}),
harvestercredentialConfig: computed('cloudCredentials.@each.harvestercredentialConfig', function() {
return (get(this, 'cloudCredentials') || []).mapBy('harvestercredentialConfig')
}),
currentCredential: computed('cloudCredentials', 'harvestercredentialConfig.[]', 'model.cloudCredentialId', function() {
return (get(this, 'cloudCredentials') || []).find((C) => C.id === get(this, 'model.cloudCredentialId'));
}),
isSystemNamespace(namespace) {
if ( namespace.metadata && namespace.metadata.annotations && namespace.metadata.annotations['management.cattle.io/system-namespace'] === 'true' ) {
return true;
}
if (namespace.metadata.labels['fleet.cattle.io/managed'] === 'true') {
return true;
}
if ( SYSTEM_NAMESPACES.includes(namespace.metadata.name) ) {
return true;
}
if ( namespace.metadata && namespace.metadata.name && namespace.metadata.name.endsWith('-system') ) {
return true;
}
return false;
},
bootstrap() {
let config = get(this, 'globalStore').createRecord({
type: CONFIG,
cpuCount: 2,
memorySize: 4,
diskSize: 40,
diskBus: 'virtio',
imageName: '',
sshUser: '',
networkName: '',
networkData: '',
vmNamespace: '',
userData: '',
vmAffinity: '',
diskInfo: '',
networkInfo: ''
});
set(this, `model.${ CONFIG }`, config);
},
validate() {
this._super();
let errors = get(this, 'errors') || [];
if (!this.validateCloudCredentials()) {
errors.push(this.intl.t('nodeDriver.cloudCredentialError'))
}
if (!get(this, 'config.vmNamespace')) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.namespace.label') }));
}
if (!get(this, 'config.sshUser')) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.sshUser.label') }));
}
this.validateScheduling(errors);
this.validateDiskAndNetwork(errors);
// Set the array of errors for display,
// and return true if saving should continue.
if (errors.length) {
set(this, 'errors', errors.uniq());
return false;
}
return true;
},
isReady() {
function getStatusConditionOfType(type, defaultValue = []) {
const conditions = Array.isArray(get(this, 'status.conditions')) ? this.status.conditions : defaultValue;
return conditions.find( (cond) => cond.type === type);
}
const initialized = getStatusConditionOfType.call(this, 'Initialized');
const imported = getStatusConditionOfType.call(this, 'Imported');
const isCompleted = this.status?.progress === 100;
if ([initialized?.status, imported?.status].includes('False')) {
return false;
} else {
return isCompleted && true;
}
},
isEmptyObject(obj) {
return obj
&& Object.keys(obj).length === 0
&& Object.getPrototypeOf(obj) === Object.prototype;
},
isImageVolume(volume) {
return Object.prototype.hasOwnProperty.call(volume, 'imageName');
},
initSchedulings() {
const nodeSchedulings = [];
const podSchedulings = [];
const parsedObj = JSON.parse(AWS.util.base64.decode(get(this, 'config.vmAffinity')).toString());
const nodeAffinityRequired = get(parsedObj, 'nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution');
const nodeAffinityPreferred = get(parsedObj, 'nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution');
const podAffinityRequired = get(parsedObj, 'podAffinity.requiredDuringSchedulingIgnoredDuringExecution');
const podAffinityPreferred = get(parsedObj, 'podAffinity.preferredDuringSchedulingIgnoredDuringExecution');
const podAntiAffinityRequired = get(parsedObj, 'podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution');
const podAntiAffinityPreferred = get(parsedObj, 'podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution');
if (nodeAffinityRequired) {
nodeAffinityRequired.nodeSelectorTerms.forEach((S) => {
nodeSchedulings.push({
priority: PRIORITY.REQUIRED,
nodeSelectorTerms: { matchExpressions: S.matchExpressions },
})
});
}
if (nodeAffinityPreferred) {
nodeAffinityPreferred.forEach((S) => {
nodeSchedulings.push({
priority: PRIORITY.PREFERRED,
nodeSelectorTerms: { matchExpressions: S.preference.matchExpressions },
})
});
}
if (podAffinityRequired) {
podAffinityRequired.forEach((S) => {
podSchedulings.push({
type: TYPE.AFFINITY,
priority: PRIORITY.REQUIRED,
labelSelector: { matchExpressions: S.labelSelector.matchExpressions },
topologyKey: S.topologyKey,
namespaces: S.namespaces || [],
weight: S.weight || ''
})
});
}
if (podAffinityPreferred) {
podAffinityPreferred.forEach((S) => {
podSchedulings.push({
type: TYPE.AFFINITY,
priority: PRIORITY.PREFERRED,
labelSelector: { matchExpressions: S.podAffinityTerm.labelSelector.matchExpressions },
topologyKey: S.podAffinityTerm.topologyKey,
namespaces: get(S, 'podAffinityTerm.namespaces') || [],
weight: get(S, 'podAffinityTerm.weight') || ''
})
});
}
if (podAntiAffinityRequired) {
podAntiAffinityRequired.forEach((S) => {
podSchedulings.push({
type: TYPE.ANTI_AFFINITY,
priority: PRIORITY.REQUIRED,
labelSelector: { matchExpressions: S.labelSelector.matchExpressions },
topologyKey: S.topologyKey,
namespaces: S.namespaces || [],
weight: S.weight || ''
})
});
}
if (podAntiAffinityPreferred) {
podAntiAffinityPreferred.forEach((S) => {
podSchedulings.push({
type: TYPE.ANTI_AFFINITY,
priority: PRIORITY.PREFERRED,
labelSelector: { matchExpressions: S.podAffinityTerm.labelSelector.matchExpressions },
topologyKey: S.podAffinityTerm.topologyKey,
namespaces: get(S, 'podAffinityTerm.namespaces') || [],
weight: get(S, 'podAffinityTerm.weight') || ''
})
});
}
set(this, 'nodeSchedulings', nodeSchedulings);
set(this, 'podSchedulings', podSchedulings);
},
initDisks() {
let disks = [];
if (!get(this, 'config.diskInfo')) {
const imageName = get(this, 'config.imageName') || '';
disks = [{
imageName,
bootOrder: 1,
size: 40,
}];
if (get(this, 'config.diskBus')) {
disks[0].bus = get(this, 'config.diskBus');
}
const diskInfo = JSON.stringify({ disks });
set(this, 'config.diskInfo', diskInfo);
} else {
const diskInfo = get(this, 'config.diskInfo');
disks = JSON.parse(diskInfo).disks || [];
}
set(this, 'disks', disks);
},
initInterfaces() {
let _interfaces = [];
if (!get(this, 'config.networkInfo')) {
const networkName = get(this, 'config.networkName') || '';
_interfaces = [{
networkName,
macAddress: '',
}];
if (get(this, 'config.networkModel')) {
_interfaces[0].model = get(this, 'config.networkModel');
}
const networkInfo = JSON.stringify({ interfaces: _interfaces });
set(this, 'config.networkInfo', networkInfo);
} else {
const networkInfo = get(this, 'config.networkInfo');
_interfaces = JSON.parse(networkInfo).interfaces || [];
}
set(this, 'interfaces', _interfaces);
},
parseNodeScheduling() {
const arr = this.nodeSchedulings;
const out = {};
if (arr.find((S) => S.priority === PRIORITY.REQUIRED)) {
out.requiredDuringSchedulingIgnoredDuringExecution = { nodeSelectorTerms: [] }
}
if (arr.find((S) => S.priority === PRIORITY.PREFERRED)) {
out.preferredDuringSchedulingIgnoredDuringExecution = [];
}
arr.forEach((S) => {
if (S.priority === PRIORITY.REQUIRED) {
out.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.push({ matchExpressions: S.nodeSelectorTerms.matchExpressions })
}
if (S.priority === PRIORITY.PREFERRED) {
out.preferredDuringSchedulingIgnoredDuringExecution.push({ preference: { matchExpressions: S.nodeSelectorTerms.matchExpressions } })
}
})
const parseObj = { ...get(this, 'vmAffinity') };
if (!this.isEmptyObject(out)) {
set(parseObj, 'nodeAffinity', out);
} else {
delete parseObj.nodeAffinity;
}
set(this, 'config.vmAffinity', this.isEmptyObject(parseObj) ? '' : AWS.util.base64.encode(JSON.stringify(parseObj)));
set(this, 'vmAffinity', parseObj);
},
parsePodScheduling() {
const arr = this.podSchedulings;
const out = {};
if (arr.find((S) => S.type === TYPE.AFFINITY)) {
out.podAffinity = {};
}
if (arr.find((S) => S.type === TYPE.ANTI_AFFINITY)) {
out.podAntiAffinity = {};
}
if (arr.find((S) => S.type === TYPE.AFFINITY && S.priority === PRIORITY.REQUIRED)) {
out.podAffinity.requiredDuringSchedulingIgnoredDuringExecution = [];
}
if (arr.find((S) => S.type === TYPE.AFFINITY && S.priority === PRIORITY.PREFERRED)) {
out.podAffinity.preferredDuringSchedulingIgnoredDuringExecution = [];
}
if (arr.find((S) => S.type === TYPE.ANTI_AFFINITY && S.priority === PRIORITY.REQUIRED)) {
out.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution = [];
}
if (arr.find((S) => S.type === TYPE.ANTI_AFFINITY && S.priority === PRIORITY.PREFERRED)) {
out.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution = [];
}
arr.forEach((S) => {
const requiredObj = {
labelSelector: S.labelSelector,
topologyKey: S.topologyKey,
};
const preferredObj = {
podAffinityTerm: {
labelSelector: S.labelSelector,
topologyKey: S.topologyKey
}
}
if (S.namespaces) {
requiredObj.namespaces = S.namespaces;
preferredObj.podAffinityTerm.namespaces = S.namespaces;
}
if (S.weight) {
requiredObj.weight = S.weight;
preferredObj.weight = S.weight;
}
if (S.type === TYPE.AFFINITY && S.priority === PRIORITY.REQUIRED) {
out.podAffinity.requiredDuringSchedulingIgnoredDuringExecution.push(requiredObj);
}
if (S.type === TYPE.AFFINITY && S.priority === PRIORITY.PREFERRED) {
out.podAffinity.preferredDuringSchedulingIgnoredDuringExecution.push(preferredObj);
}
if (S.type === TYPE.ANTI_AFFINITY && S.priority === PRIORITY.REQUIRED) {
out.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution.push(requiredObj)
}
if (S.type === TYPE.ANTI_AFFINITY && S.priority === PRIORITY.PREFERRED) {
out.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.push(preferredObj);
}
});
const parseObj = { ...get(this, 'vmAffinity') }
if (!this.isEmptyObject(get(out, 'podAffinity') || {})) {
set(parseObj, 'podAffinity', get(out, 'podAffinity'));
} else {
delete parseObj.podAffinity;
}
if (!this.isEmptyObject(get(out, 'podAntiAffinity') || {})) {
set(parseObj, 'podAntiAffinity', get(out, 'podAntiAffinity'));
} else {
delete parseObj.podAntiAffinity;
}
set(this, 'config.vmAffinity', this.isEmptyObject(parseObj) ? '' : AWS.util.base64.encode(JSON.stringify(parseObj)));
set(this, 'vmAffinity', parseObj);
},
validateScheduling(errors) {
if (get(this, 'podSchedulings').find((S) => !S.topologyKey)) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.scheduling.input.topology.label') }));
}
const nodeHasMissingKey = get(this, 'nodeSchedulings').find((S) => {
return get(S, 'nodeSelectorTerms.matchExpressions').find((M) => !get(M, 'key'));
});
const podHasMissingKey = get(this, 'podSchedulings').find((S) => {
return get(S, 'labelSelector.matchExpressions').find((M) => !get(M, 'key'));
});
if (nodeHasMissingKey || podHasMissingKey) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('formNodeRequirement.key.label') }));
}
},
isValidMac(value) {
return /^[A-Fa-f0-9]{2}(-[A-Fa-f0-9]{2}){5}$|^[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){5}$/.test(value);
},
validateDiskAndNetwork(errors) {
const disks = get(this, 'disks');
disks.forEach((disk) => {
if (Object.prototype.hasOwnProperty.call(disk, 'imageName') && !disk.imageName) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.imageName.label') }));
}
if (Object.prototype.hasOwnProperty.call(disk, 'storageClassName') && !disk.storageClassName) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.storageClass.label') }));
}
if (!disk.size) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.diskSize.label') }));
}
if (!disk.size) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.diskSize.label') }));
}
});
const interfaces = get(this, 'interfaces');
interfaces.forEach((_interface) => {
if (!_interface.networkName) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.networkName.label') }));
}
if (_interface.macAddress && !this.isValidMac(_interface.macAddress)) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.network.macFormat') }));
}
});
}
});