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
pull/4214/head
xAt0mZ 2020-08-14 01:56:53 +02:00 committed by GitHub
parent 32bac9ffcc
commit 52bdcf2e2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 451 additions and 190 deletions

View File

@ -49,7 +49,7 @@ function _apiPortsToPublishedPorts(pList, pRefs) {
}
class KubernetesApplicationConverter {
static applicationCommon(res, data, service, ingresses) {
static applicationCommon(res, data, pods, service, ingresses) {
const containers = _.without(_.concat(data.spec.template.spec.containers, data.spec.template.spec.initContainers), undefined);
res.Id = data.metadata.uid;
res.Name = data.metadata.name;
@ -61,6 +61,7 @@ class KubernetesApplicationConverter {
res.Image = containers[0].image;
res.CreationDate = data.metadata.creationTimestamp;
res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined);
res.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, data);
const limits = {
Cpu: 0,
@ -212,9 +213,9 @@ class KubernetesApplicationConverter {
);
}
static apiDeploymentToApplication(data, service, ingresses) {
static apiDeploymentToApplication(data, pods, service, ingresses) {
const res = new KubernetesApplication();
KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses);
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT;
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
@ -223,9 +224,9 @@ class KubernetesApplicationConverter {
return res;
}
static apiDaemonSetToApplication(data, service, ingresses) {
static apiDaemonSetToApplication(data, pods, service, ingresses) {
const res = new KubernetesApplication();
KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses);
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
res.ApplicationType = KubernetesApplicationTypes.DAEMONSET;
res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL;
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
@ -234,9 +235,9 @@ class KubernetesApplicationConverter {
return res;
}
static apiStatefulSetToapplication(data, service, ingresses) {
static apiStatefulSetToapplication(data, pods, service, ingresses) {
const res = new KubernetesApplication();
KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses);
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
res.ApplicationType = KubernetesApplicationTypes.STATEFULSET;
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;
@ -246,7 +247,7 @@ class KubernetesApplicationConverter {
return res;
}
static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims) {
static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims, nodesLabels) {
const res = new KubernetesApplicationFormValues();
res.ApplicationType = app.ApplicationType;
res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]);
@ -276,6 +277,8 @@ class KubernetesApplicationConverter {
} else {
res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
}
KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity, nodesLabels);
return res;
}

View File

@ -30,6 +30,7 @@ class KubernetesDaemonSetConverter {
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
KubernetesApplicationHelper.generateAffinityFromPlacements(res, formValues);
return res;
}
@ -51,6 +52,7 @@ class KubernetesDaemonSetConverter {
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName;
payload.spec.template.spec.containers[0].name = daemonSet.Name;
payload.spec.template.spec.containers[0].image = daemonSet.Image;
payload.spec.template.spec.affinity = daemonSet.Affinity;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', daemonSet.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', daemonSet.VolumeMounts);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', daemonSet.Volumes);

View File

@ -32,6 +32,7 @@ class KubernetesDeploymentConverter {
res.Containers = formValues.Containers;
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
KubernetesApplicationHelper.generateAffinityFromPlacements(res, formValues);
return res;
}
@ -53,6 +54,7 @@ class KubernetesDeploymentConverter {
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName;
payload.spec.template.spec.containers[0].name = deployment.Name;
payload.spec.template.spec.containers[0].image = deployment.Image;
payload.spec.template.spec.affinity = deployment.Affinity;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', deployment.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', deployment.VolumeMounts);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', deployment.Volumes);

View File

@ -33,6 +33,7 @@ class KubernetesStatefulSetConverter {
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
KubernetesApplicationHelper.generateAffinityFromPlacements(res, formValues);
return res;
}
@ -56,6 +57,7 @@ class KubernetesStatefulSetConverter {
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName;
payload.spec.template.spec.containers[0].name = statefulSet.Name;
payload.spec.template.spec.containers[0].image = statefulSet.Image;
payload.spec.template.spec.affinity = statefulSet.Affinity;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', statefulSet.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', statefulSet.VolumeMounts);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', statefulSet.Volumes);

View File

@ -119,4 +119,15 @@ angular
}
return '';
};
})
.filter('kubernetesNodeLabelHumanReadbleText', function () {
'use strict';
return function (text) {
const values = {
'kubernetes.io/os': 'Operating system',
'kubernetes.io/arch': 'Architecture',
'kubernetes.io/hostname': 'Node',
};
return values[text] || text;
};
});

View File

@ -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;

View File

@ -1,4 +1,4 @@
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes } from './models';
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes, KubernetesApplicationPlacementTypes } from './models';
/**
* KubernetesApplicationFormValues Model
@ -10,19 +10,21 @@ const _KubernetesApplicationFormValues = Object.freeze({
StackName: '',
ApplicationOwner: '',
Image: '',
ReplicaCount: 1,
Note: '',
EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list
PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list
PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list
MemoryLimit: 0,
CpuLimit: 0,
DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED,
PublishingType: KubernetesApplicationPublishingTypes.INTERNAL,
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED,
Configurations: [], // KubernetesApplicationConfigurationFormValue list
Containers: [],
ReplicaCount: 1,
AutoScaler: {},
Containers: [],
EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED,
PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list
Configurations: [], // KubernetesApplicationConfigurationFormValue list
PublishingType: KubernetesApplicationPublishingTypes.INTERNAL,
PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list
PlacementType: KubernetesApplicationPlacementTypes.PREFERRED,
Placements: [], // KubernetesApplicationPlacementFormValue list
OriginalIngresses: undefined,
});
@ -123,6 +125,15 @@ export function KubernetesApplicationPublishedPortFormValue() {
};
}
export function KubernetesApplicationPlacementFormValue() {
return {
Label: {},
Value: '',
NeedsDeletion: false,
IsNew: true,
};
}
/**
* KubernetesApplicationAutoScalerFormValue Model
*/

View File

@ -27,6 +27,11 @@ export const KubernetesApplicationPublishingTypes = Object.freeze({
INGRESS: 4,
});
export const KubernetesApplicationPlacementTypes = Object.freeze({
PREFERRED: 1,
MANDATORY: 2,
});
export const KubernetesApplicationQuotaDefaults = {
CpuLimit: 0.1,
MemoryLimit: 64, // MB

View File

@ -15,6 +15,7 @@ const _KubernetesDaemonSet = Object.freeze({
ApplicationName: '',
ApplicationOwner: '',
Note: '',
Affinity: undefined, // KubernetesPodAffinity
});
export class KubernetesDaemonSet {

View File

@ -16,6 +16,7 @@ const _KubernetesDeployment = Object.freeze({
ApplicationName: '',
ApplicationOwner: '',
Note: '',
Affinity: undefined, // KubernetesPodAffinity
});
export class KubernetesDeployment {

View File

@ -26,6 +26,7 @@ const _KubernetesDeploymentCreatePayload = Object.freeze({
},
},
spec: {
affinity: {},
containers: [
{
name: '',

View File

@ -18,6 +18,7 @@ const _KubernetesStatefulSet = Object.freeze({
ApplicationName: '',
ApplicationOwner: '',
Note: '',
Affinity: undefined, // KubernetesPodAffinity
});
export class KubernetesStatefulSet {

View File

@ -20,4 +20,15 @@ export class KubernetesNodeHelper {
return label;
});
}
static generateNodeLabelsFromNodes(nodes) {
const pairs = _.flatMap(nodes, (node) => {
return _.map(_.toPairs(node.Labels), ([k, v]) => {
return { key: k, value: v };
});
});
return _.map(_.groupBy(pairs, 'key'), (values, key) => {
return { Key: key, Values: _.uniq(_.map(values, 'value')) };
});
}
}

View File

@ -31,7 +31,7 @@ function computeContainerStatus(statuses, name) {
function computeAffinity(affinity) {
const res = new KubernetesPodAffinity();
if (affinity) {
res.NodeAffinity = affinity.nodeAffinity || {};
res.nodeAffinity = affinity.nodeAffinity || {};
}
return res;
}

View File

@ -10,56 +10,13 @@ export const KubernetesPodNodeAffinityNodeSelectorRequirementOperators = Object.
/**
* KubernetesPodAffinity Model
*/
const _KubernetesPodAffinity = Object.freeze({
NodeAffinity: {},
// PodAffinity: {},
// PodAntiAffinity: {},
});
export class KubernetesPodAffinity {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodAffinity)));
}
}
/**
* KubernetesPodNodeAffinity Model
*/
const _KubernetesPodNodeAffinity = Object.freeze({
PreferredDuringSchedulingIgnoredDuringExecution: [],
RequiredDuringSchedulingIgnoredDuringExecution: {},
});
export class KubernetesPodNodeAffinity {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodNodeAffinity)));
}
}
/**
* KubernetesPodPodAffinity Model
*/
const _KubernetesPodPodAffinity = Object.freeze({
PreferredDuringSchedulingIgnoredDuringExecution: [],
equiredDuringSchedulingIgnoredDuringExecution: [],
});
export class KubernetesPodPodAffinity {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodPodAffinity)));
}
}
/**
* KubernetesPodPodAntiAffinity Model
*/
const _KubernetesPodPodAntiAffinity = Object.freeze({
preferredDuringSchedulingIgnoredDuringExecution: [],
requiredDuringSchedulingIgnoredDuringExecution: [],
});
export class KubernetesPodPodAntiAffinity {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodPodAntiAffinity)));
}
// this model will contain non transformed data (raw payload data)
// either during creation flow (model > api)
// than during reading flow (api > model)
export function KubernetesPodAffinity() {
return {
nodeAffinity: {}, // KubernetesPodNodeAffinityPayload
// podAffinity: {},
// podAntiAffinity: {},
};
}

View File

@ -16,6 +16,7 @@ const _KubernetesPod = Object.freeze({
Labels: [],
Affinity: {}, // KubernetesPodAffinity
Tolerations: [], // KubernetesPodToleration[]
NodeSelector: undefined,
});
export class KubernetesPod {

View File

@ -0,0 +1,29 @@
export function KubernetesPodNodeAffinityPayload() {
return {
requiredDuringSchedulingIgnoredDuringExecution: {
nodeSelectorTerms: [], // []KubernetesNodeSelectorTermPayload
},
preferredDuringSchedulingIgnoredDuringExecution: [], // []KubernetesPreferredSchedulingTermPayload
};
}
export function KubernetesPreferredSchedulingTermPayload() {
return {
weight: 1,
preference: {}, // KubernetesNodeSelectorTermPayload
};
}
export function KubernetesNodeSelectorTermPayload() {
return {
matchExpressions: [], // []KubernetesNodeSelectorRequirementPayload
};
}
export function KubernetesNodeSelectorRequirementPayload() {
return {
key: '', // string
operator: '', // KubernetesPodNodeAffinityNodeSelectorRequirementOperators
values: [], // []string
};
}

View File

@ -111,10 +111,9 @@ class KubernetesApplicationService {
const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw);
const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {};
const application = converterFunc(rootItem.value.Raw, service.Raw, ingresses.value);
const application = converterFunc(rootItem.value.Raw, pods.value, service.Raw, ingresses.value);
application.Yaml = rootItem.value.Yaml;
application.Raw = rootItem.value.Raw;
application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods.value, application.Raw);
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application);
@ -143,8 +142,7 @@ class KubernetesApplicationService {
const convertToApplication = (item, converterFunc, services, pods, ingresses) => {
const service = KubernetesServiceHelper.findApplicationBoundService(services, item);
const application = converterFunc(item, service, ingresses);
application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item);
const application = converterFunc(item, pods, service, ingresses);
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
return application;
};

View File

@ -926,6 +926,102 @@
</div>
<!-- #endregion -->
<div ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.REPLICATED">
<div class="col-sm-12 form-section-title">
Placement preferences and constraints
</div>
<!-- #region PLACEMENTS -->
<div class="form-group">
<div class="col-sm-12 small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Deploy this application on nodes that respect all of the following placement rules.
</div>
</div>
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Placement rules</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addPlacement()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add rule
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="placement in ctrl.formValues.Placements" style="margin-top: 2px;">
<div class="col-sm-5 input-group" ng-class="{ striked: placement.NeedsDeletion }">
<select
class="form-control"
ng-model="placement.Label"
ng-options="label as (label.Key | kubernetesNodeLabelHumanReadbleText) for label in ctrl.nodesLabels"
ng-change="ctrl.onPlacementLabelChange($index)"
ng-disabled="ctrl.isEditAndNotNewPlacement($index)"
>
</select>
</div>
<div class="col-sm-5 input-group" ng-class="{ striked: placement.NeedsDeletion }">
<select
class="form-control"
ng-model="placement.Value"
ng-options="value for value in placement.Label.Values"
ng-disabled="ctrl.isEditAndNotNewPlacement($index)"
>
</select>
</div>
<div class="col-sm-1 input-group">
<button ng-if="!placement.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePlacement($index)">
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
<button ng-if="placement.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restorePlacement($index)">
<i class="fa fa-trash-restore" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<div ng-if="ctrl.showPlacementPolicySection()">
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Placement policy</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12 small text-muted">
Specify the policy associated to the placement rules.
</div>
</div>
<!-- placement policy options -->
<div class="form-group" style="margin-bottom: 0;" ng-if="ctrl.formValues.Placements.length">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="placement_soft" ng-value="ctrl.ApplicationPlacementTypes.PREFERRED" ng-model="ctrl.formValues.PlacementType" />
<label for="placement_soft">
<div class="boxselector_header">
<i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i>
Preferred
</div>
<p>Schedule this application on nodes that match the rules if possible</p>
</label>
</div>
<div>
<input type="radio" id="placement_hard" ng-value="ctrl.ApplicationPlacementTypes.MANDATORY" ng-model="ctrl.formValues.PlacementType" />
<label for="placement_hard">
<div class="boxselector_header">
<i class="fa fa-tasks" aria-hidden="true" style="margin-right: 2px;"></i>
Mandatory
</div>
<p>Schedule this application on nodes that match the rules only</p>
</label>
</div>
</div>
</div>
<!-- !placement policy options -->
</div>
<!-- #endregion -->
</div>
<div class="col-sm-12 form-section-title">
Publishing the application
</div>

View File

@ -9,6 +9,7 @@ import {
KubernetesApplicationPublishingTypes,
KubernetesApplicationQuotaDefaults,
KubernetesApplicationTypes,
KubernetesApplicationPlacementTypes,
} from 'Kubernetes/models/application/models';
import {
KubernetesApplicationConfigurationFormValue,
@ -18,6 +19,7 @@ import {
KubernetesApplicationFormValues,
KubernetesApplicationPersistedFolderFormValue,
KubernetesApplicationPublishedPortFormValue,
KubernetesApplicationPlacementFormValue,
} from 'Kubernetes/models/application/formValues';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
@ -25,6 +27,7 @@ import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceRese
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
import { KubernetesNodeHelper } from 'Kubernetes/node/helper';
class KubernetesCreateApplicationController {
/* #region CONSTRUCTOR */
@ -66,6 +69,7 @@ class KubernetesCreateApplicationController {
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
this.ApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
this.ApplicationPlacementTypes = KubernetesApplicationPlacementTypes;
this.ApplicationTypes = KubernetesApplicationTypes;
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
this.ServiceTypes = KubernetesServiceTypes;
@ -248,6 +252,33 @@ class KubernetesCreateApplicationController {
}
/* #endregion */
/* #region PLACEMENT UI MANAGEMENT */
addPlacement() {
const placement = new KubernetesApplicationPlacementFormValue();
const label = this.nodesLabels[0];
placement.Label = label;
placement.Value = label.Values[0];
this.formValues.Placements.push(placement);
}
restorePlacement(index) {
this.formValues.Placements[index].NeedsDeletion = false;
}
removePlacement(index) {
if (this.state.isEdit && !this.formValues.Placements[index].IsNew) {
this.formValues.Placements[index].NeedsDeletion = true;
} else {
this.formValues.Placements.splice(index, 1);
}
}
onPlacementLabelChange(index) {
this.formValues.Placements[index].Value = this.formValues.Placements[index].Label.Values[0];
}
/* #endregion */
/* #region PUBLISHED PORTS UI MANAGEMENT */
addPublishedPort() {
const p = new KubernetesApplicationPublishedPortFormValue();
@ -512,6 +543,15 @@ class KubernetesCreateApplicationController {
return this.formValues.PublishingType !== KubernetesApplicationPublishingTypes.INTERNAL && toKeepPorts.length === 0;
}
isEditAndNotNewPlacement(index) {
return this.state.isEdit && !this.formValues.Placements[index].IsNew;
}
showPlacementPolicySection() {
const placements = _.filter(this.formValues.Placements, { NeedsDeletion: false });
return placements.length !== 0;
}
isNonScalable() {
const scalable = this.supportScalableReplicaDeployment();
const global = this.supportGlobalDeployment();
@ -861,13 +901,20 @@ class KubernetesCreateApplicationController {
this.state.nodes.memory += filesizeParser(item.Memory);
this.state.nodes.cpu += item.CPU;
});
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
await this.refreshNamespaceData(namespace);
if (this.state.isEdit) {
await this.getApplication();
this.formValues = KubernetesApplicationConverter.applicationToFormValues(this.application, this.resourcePools, this.configurations, this.persistentVolumeClaims);
this.formValues = KubernetesApplicationConverter.applicationToFormValues(
this.application,
this.resourcePools,
this.configurations,
this.persistentVolumeClaims,
this.nodesLabels
);
this.formValues.OriginalIngresses = this.filteredIngresses;
this.savedFormValues = angular.copy(this.formValues);
delete this.formValues.ApplicationType;

View File

@ -53,8 +53,8 @@ function computeAffinities(nodes, application) {
}
}
if (pod.Affinity.NodeAffinity.requiredDuringSchedulingIgnoredDuringExecution) {
const unmatchedTerms = _.map(pod.Affinity.NodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, (t) => {
if (pod.Affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution) {
const unmatchedTerms = _.map(pod.Affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, (t) => {
const unmatchedExpressions = _.map(t.matchExpressions, (e) => {
const exists = {}.hasOwnProperty.call(n.Labels, e.key);
const isIn = exists && _.includes(e.values, n.Labels[e.key]);

View File

@ -94,7 +94,7 @@
<!-- ADMIN + UNMET TAINTS -->
<tr ng-if="$ctrl.isAdmin" ng-show="item.Expanded" ng-repeat="taint in item.UnmetTaints" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
<td colspan="2">
This application is missing a toleration for the taint <code>{{ taint.Key }}{{ taint.Value ? '=' + taint.Value : '' }}:{{ taint.effect }}</code>
This application is missing a toleration for the taint <code>{{ taint.Key }}{{ taint.Value ? '=' + taint.Value : '' }}:{{ taint.Effect }}</code>
</td>
</tr>
<!-- !ADMIN + UNMET TAINTS -->