mirror of
https://github.com/portainer/portainer.git
synced 2025-07-23 15:29:42 +02:00
feat(k8s/application): add/edit placement preferences/constraints (#4210)
* feat(k8s/application): create application with placement preferences/constraints * feat(k8s/application): edit application placement preferences/constraints
This commit is contained in:
parent
32bac9ffcc
commit
52bdcf2e2b
22 changed files with 451 additions and 190 deletions
|
@ -1,4 +1,4 @@
|
|||
import _ from 'lodash-es';
|
||||
import * as _ from 'lodash-es';
|
||||
import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models';
|
||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||
|
@ -10,6 +10,7 @@ import {
|
|||
KubernetesApplicationPersistedFolderFormValue,
|
||||
KubernetesApplicationPublishedPortFormValue,
|
||||
KubernetesApplicationAutoScalerFormValue,
|
||||
KubernetesApplicationPlacementFormValue,
|
||||
} from 'Kubernetes/models/application/formValues';
|
||||
import {
|
||||
KubernetesApplicationEnvConfigMapPayload,
|
||||
|
@ -22,8 +23,21 @@ import {
|
|||
KubernetesApplicationVolumeSecretPayload,
|
||||
} from 'Kubernetes/models/application/payloads';
|
||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||
import { KubernetesApplicationPlacementTypes, KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
|
||||
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators, KubernetesPodAffinity } from 'Kubernetes/pod/models';
|
||||
import {
|
||||
KubernetesNodeSelectorTermPayload,
|
||||
KubernetesPreferredSchedulingTermPayload,
|
||||
KubernetesPodNodeAffinityPayload,
|
||||
KubernetesNodeSelectorRequirementPayload,
|
||||
} from 'Kubernetes/pod/payloads/affinities';
|
||||
|
||||
class KubernetesApplicationHelper {
|
||||
/* #region UTILITY FUNCTIONS */
|
||||
static isExternalApplication(application) {
|
||||
return !application.ApplicationOwner;
|
||||
}
|
||||
|
||||
static associatePodsAndApplication(pods, app) {
|
||||
return _.filter(pods, { Labels: app.spec.selector.matchLabels });
|
||||
}
|
||||
|
@ -93,10 +107,9 @@ class KubernetesApplicationHelper {
|
|||
);
|
||||
return res;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/**
|
||||
* FORMVALUES TO APPLICATION FUNCTIONS
|
||||
*/
|
||||
/* #region ENV VARIABLES FV <> ENV */
|
||||
static generateEnvFromEnvVariables(envVariables) {
|
||||
_.remove(envVariables, (item) => item.NeedsDeletion);
|
||||
const env = _.map(envVariables, (item) => {
|
||||
|
@ -108,6 +121,75 @@ class KubernetesApplicationHelper {
|
|||
return env;
|
||||
}
|
||||
|
||||
static generateEnvVariablesFromEnv(env) {
|
||||
const envVariables = _.map(env, (item) => {
|
||||
if (!item.value) {
|
||||
return;
|
||||
}
|
||||
const res = new KubernetesApplicationEnvironmentVariableFormValue();
|
||||
res.Name = item.name;
|
||||
res.Value = item.value;
|
||||
res.IsNew = false;
|
||||
return res;
|
||||
});
|
||||
return _.without(envVariables, undefined);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region CONFIGURATIONS FV <> ENV & VOLUMES */
|
||||
static generateConfigurationFormValuesFromEnvAndVolumes(env, volumes, configurations) {
|
||||
const finalRes = _.flatMap(configurations, (cfg) => {
|
||||
const filterCondition = cfg.Type === KubernetesConfigurationTypes.CONFIGMAP ? 'valueFrom.configMapKeyRef.name' : 'valueFrom.secretKeyRef.name';
|
||||
|
||||
const cfgEnv = _.filter(env, [filterCondition, cfg.Name]);
|
||||
const cfgVol = _.filter(volumes, { configurationName: cfg.Name });
|
||||
if (!cfgEnv.length && !cfgVol.length) {
|
||||
return;
|
||||
}
|
||||
const keys = _.reduce(
|
||||
_.keys(cfg.Data),
|
||||
(acc, k) => {
|
||||
const keyEnv = _.filter(cfgEnv, { name: k });
|
||||
const keyVol = _.filter(cfgVol, { configurationKey: k });
|
||||
const key = {
|
||||
Key: k,
|
||||
Count: keyEnv.length + keyVol.length,
|
||||
Sum: _.concat(keyEnv, keyVol),
|
||||
EnvCount: keyEnv.length,
|
||||
VolCount: keyVol.length,
|
||||
};
|
||||
acc.push(key);
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const max = _.max(_.map(keys, 'Count'));
|
||||
const overrideThreshold = max - _.max(_.map(keys, 'VolCount'));
|
||||
const res = _.map(new Array(max), () => new KubernetesApplicationConfigurationFormValue());
|
||||
_.forEach(res, (item, index) => {
|
||||
item.SelectedConfiguration = cfg;
|
||||
const overriden = index >= overrideThreshold;
|
||||
if (overriden) {
|
||||
item.Overriden = true;
|
||||
item.OverridenKeys = _.map(keys, (k) => {
|
||||
const fvKey = new KubernetesApplicationConfigurationFormValueOverridenKey();
|
||||
fvKey.Key = k.Key;
|
||||
if (index < k.EnvCount) {
|
||||
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT;
|
||||
} else {
|
||||
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM;
|
||||
fvKey.Path = k.Sum[index].rootMountPath;
|
||||
}
|
||||
return fvKey;
|
||||
});
|
||||
}
|
||||
});
|
||||
return res;
|
||||
});
|
||||
return _.without(finalRes, undefined);
|
||||
}
|
||||
|
||||
static generateEnvOrVolumesFromConfigurations(app, configurations) {
|
||||
let finalEnv = [];
|
||||
let finalVolumes = [];
|
||||
|
@ -181,110 +263,9 @@ class KubernetesApplicationHelper {
|
|||
app.VolumeMounts = _.concat(app.VolumeMounts, finalMounts);
|
||||
return app;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
static generateVolumesFromPersistentVolumClaims(app, volumeClaims) {
|
||||
app.VolumeMounts = [];
|
||||
app.Volumes = [];
|
||||
_.forEach(volumeClaims, (item) => {
|
||||
const volumeMount = new KubernetesApplicationVolumeMountPayload();
|
||||
const name = item.Name;
|
||||
volumeMount.name = name;
|
||||
volumeMount.mountPath = item.MountPath;
|
||||
app.VolumeMounts.push(volumeMount);
|
||||
|
||||
const volume = new KubernetesApplicationVolumePersistentPayload();
|
||||
volume.name = name;
|
||||
volume.persistentVolumeClaim.claimName = name;
|
||||
app.Volumes.push(volume);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* !FORMVALUES TO APPLICATION FUNCTIONS
|
||||
*/
|
||||
|
||||
/**
|
||||
* APPLICATION TO FORMVALUES FUNCTIONS
|
||||
*/
|
||||
static generateEnvVariablesFromEnv(env) {
|
||||
const envVariables = _.map(env, (item) => {
|
||||
if (!item.value) {
|
||||
return;
|
||||
}
|
||||
const res = new KubernetesApplicationEnvironmentVariableFormValue();
|
||||
res.Name = item.name;
|
||||
res.Value = item.value;
|
||||
res.IsNew = false;
|
||||
return res;
|
||||
});
|
||||
return _.without(envVariables, undefined);
|
||||
}
|
||||
|
||||
static generateConfigurationFormValuesFromEnvAndVolumes(env, volumes, configurations) {
|
||||
const finalRes = _.flatMap(configurations, (cfg) => {
|
||||
const filterCondition = cfg.Type === KubernetesConfigurationTypes.CONFIGMAP ? 'valueFrom.configMapKeyRef.name' : 'valueFrom.secretKeyRef.name';
|
||||
|
||||
const cfgEnv = _.filter(env, [filterCondition, cfg.Name]);
|
||||
const cfgVol = _.filter(volumes, { configurationName: cfg.Name });
|
||||
if (!cfgEnv.length && !cfgVol.length) {
|
||||
return;
|
||||
}
|
||||
const keys = _.reduce(
|
||||
_.keys(cfg.Data),
|
||||
(acc, k) => {
|
||||
const keyEnv = _.filter(cfgEnv, { name: k });
|
||||
const keyVol = _.filter(cfgVol, { configurationKey: k });
|
||||
const key = {
|
||||
Key: k,
|
||||
Count: keyEnv.length + keyVol.length,
|
||||
Sum: _.concat(keyEnv, keyVol),
|
||||
EnvCount: keyEnv.length,
|
||||
VolCount: keyVol.length,
|
||||
};
|
||||
acc.push(key);
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const max = _.max(_.map(keys, 'Count'));
|
||||
const overrideThreshold = max - _.max(_.map(keys, 'VolCount'));
|
||||
const res = _.map(new Array(max), () => new KubernetesApplicationConfigurationFormValue());
|
||||
_.forEach(res, (item, index) => {
|
||||
item.SelectedConfiguration = cfg;
|
||||
const overriden = index >= overrideThreshold;
|
||||
if (overriden) {
|
||||
item.Overriden = true;
|
||||
item.OverridenKeys = _.map(keys, (k) => {
|
||||
const fvKey = new KubernetesApplicationConfigurationFormValueOverridenKey();
|
||||
fvKey.Key = k.Key;
|
||||
if (index < k.EnvCount) {
|
||||
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT;
|
||||
} else {
|
||||
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM;
|
||||
fvKey.Path = k.Sum[index].rootMountPath;
|
||||
}
|
||||
return fvKey;
|
||||
});
|
||||
}
|
||||
});
|
||||
return res;
|
||||
});
|
||||
return _.without(finalRes, undefined);
|
||||
}
|
||||
|
||||
static generatePersistedFoldersFormValuesFromPersistedFolders(persistedFolders, persistentVolumeClaims) {
|
||||
const finalRes = _.map(persistedFolders, (folder) => {
|
||||
const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.PersistentVolumeClaimName));
|
||||
const res = new KubernetesApplicationPersistedFolderFormValue(pvc.StorageClass);
|
||||
res.PersistentVolumeClaimName = folder.PersistentVolumeClaimName;
|
||||
res.Size = parseInt(pvc.Storage.slice(0, -2));
|
||||
res.SizeUnit = pvc.Storage.slice(-2);
|
||||
res.ContainerPath = folder.MountPath;
|
||||
return res;
|
||||
});
|
||||
return finalRes;
|
||||
}
|
||||
|
||||
/* #region PUBLISHED PORTS FV <> PUBLISHED PORTS */
|
||||
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) {
|
||||
const generatePort = (port, rule) => {
|
||||
const res = new KubernetesApplicationPublishedPortFormValue();
|
||||
|
@ -313,7 +294,9 @@ class KubernetesApplicationHelper {
|
|||
});
|
||||
return finalRes;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region AUTOSCALER FV <> HORIZONTAL POD AUTOSCALER */
|
||||
static generateAutoScalerFormValueFromHorizontalPodAutoScaler(autoScaler, replicasCount) {
|
||||
const res = new KubernetesApplicationAutoScalerFormValue();
|
||||
if (autoScaler) {
|
||||
|
@ -330,12 +313,111 @@ class KubernetesApplicationHelper {
|
|||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* !APPLICATION TO FORMVALUES FUNCTIONS
|
||||
*/
|
||||
/* #endregion */
|
||||
|
||||
static isExternalApplication(application) {
|
||||
return !application.ApplicationOwner;
|
||||
/* #region PERSISTED FOLDERS FV <> VOLUMES */
|
||||
static generatePersistedFoldersFormValuesFromPersistedFolders(persistedFolders, persistentVolumeClaims) {
|
||||
const finalRes = _.map(persistedFolders, (folder) => {
|
||||
const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.PersistentVolumeClaimName));
|
||||
const res = new KubernetesApplicationPersistedFolderFormValue(pvc.StorageClass);
|
||||
res.PersistentVolumeClaimName = folder.PersistentVolumeClaimName;
|
||||
res.Size = parseInt(pvc.Storage.slice(0, -2));
|
||||
res.SizeUnit = pvc.Storage.slice(-2);
|
||||
res.ContainerPath = folder.MountPath;
|
||||
return res;
|
||||
});
|
||||
return finalRes;
|
||||
}
|
||||
|
||||
static generateVolumesFromPersistentVolumClaims(app, volumeClaims) {
|
||||
app.VolumeMounts = [];
|
||||
app.Volumes = [];
|
||||
_.forEach(volumeClaims, (item) => {
|
||||
const volumeMount = new KubernetesApplicationVolumeMountPayload();
|
||||
const name = item.Name;
|
||||
volumeMount.name = name;
|
||||
volumeMount.mountPath = item.MountPath;
|
||||
app.VolumeMounts.push(volumeMount);
|
||||
|
||||
const volume = new KubernetesApplicationVolumePersistentPayload();
|
||||
volume.name = name;
|
||||
volume.persistentVolumeClaim.claimName = name;
|
||||
app.Volumes.push(volume);
|
||||
});
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region PLACEMENTS FV <> AFFINITY */
|
||||
static generatePlacementsFormValuesFromAffinity(formValues, podAffinity, nodesLabels) {
|
||||
let placements = formValues.Placements;
|
||||
let type = formValues.PlacementType;
|
||||
const affinity = podAffinity.nodeAffinity;
|
||||
if (affinity && affinity.requiredDuringSchedulingIgnoredDuringExecution) {
|
||||
type = KubernetesApplicationPlacementTypes.MANDATORY;
|
||||
_.forEach(affinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, (term) => {
|
||||
_.forEach(term.matchExpressions, (exp) => {
|
||||
const placement = new KubernetesApplicationPlacementFormValue();
|
||||
const label = _.find(nodesLabels, { Key: exp.key });
|
||||
placement.Label = label;
|
||||
placement.Value = exp.values[0];
|
||||
placement.IsNew = false;
|
||||
placements.push(placement);
|
||||
});
|
||||
});
|
||||
} else if (affinity && affinity.preferredDuringSchedulingIgnoredDuringExecution) {
|
||||
type = KubernetesApplicationPlacementTypes.PREFERRED;
|
||||
_.forEach(affinity.preferredDuringSchedulingIgnoredDuringExecution, (term) => {
|
||||
_.forEach(term.preference.matchExpressions, (exp) => {
|
||||
const placement = new KubernetesApplicationPlacementFormValue();
|
||||
const label = _.find(nodesLabels, { Key: exp.key });
|
||||
placement.Label = label;
|
||||
placement.Value = exp.values[0];
|
||||
placement.IsNew = false;
|
||||
placements.push(placement);
|
||||
});
|
||||
});
|
||||
}
|
||||
formValues.Placements = placements;
|
||||
formValues.PlacementType = type;
|
||||
}
|
||||
|
||||
static generateAffinityFromPlacements(app, formValues) {
|
||||
if (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.REPLICATED) {
|
||||
const placements = formValues.Placements;
|
||||
const res = new KubernetesPodNodeAffinityPayload();
|
||||
let expressions = _.map(placements, (p) => {
|
||||
if (!p.NeedsDeletion) {
|
||||
const exp = new KubernetesNodeSelectorRequirementPayload();
|
||||
exp.key = p.Label.Key;
|
||||
if (p.Value) {
|
||||
exp.operator = KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN;
|
||||
exp.values = [p.Value];
|
||||
} else {
|
||||
exp.operator = KubernetesPodNodeAffinityNodeSelectorRequirementOperators.EXISTS;
|
||||
delete exp.values;
|
||||
}
|
||||
return exp;
|
||||
}
|
||||
});
|
||||
expressions = _.without(expressions, undefined);
|
||||
if (expressions.length) {
|
||||
if (formValues.PlacementType === KubernetesApplicationPlacementTypes.MANDATORY) {
|
||||
const term = new KubernetesNodeSelectorTermPayload();
|
||||
term.matchExpressions = expressions;
|
||||
res.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.push(term);
|
||||
delete res.preferredDuringSchedulingIgnoredDuringExecution;
|
||||
} else if (formValues.PlacementType === KubernetesApplicationPlacementTypes.PREFERRED) {
|
||||
const term = new KubernetesPreferredSchedulingTermPayload();
|
||||
term.preference = new KubernetesNodeSelectorTermPayload();
|
||||
term.preference.matchExpressions = expressions;
|
||||
res.preferredDuringSchedulingIgnoredDuringExecution.push(term);
|
||||
delete res.requiredDuringSchedulingIgnoredDuringExecution;
|
||||
}
|
||||
app.Affinity = new KubernetesPodAffinity();
|
||||
app.Affinity.nodeAffinity = res;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* #endregion */
|
||||
}
|
||||
export default KubernetesApplicationHelper;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue