mirror of https://github.com/portainer/portainer
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/constraintspull/4214/head
parent
32bac9ffcc
commit
52bdcf2e2b
|
@ -49,7 +49,7 @@ function _apiPortsToPublishedPorts(pList, pRefs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class KubernetesApplicationConverter {
|
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);
|
const containers = _.without(_.concat(data.spec.template.spec.containers, data.spec.template.spec.initContainers), undefined);
|
||||||
res.Id = data.metadata.uid;
|
res.Id = data.metadata.uid;
|
||||||
res.Name = data.metadata.name;
|
res.Name = data.metadata.name;
|
||||||
|
@ -61,6 +61,7 @@ class KubernetesApplicationConverter {
|
||||||
res.Image = containers[0].image;
|
res.Image = containers[0].image;
|
||||||
res.CreationDate = data.metadata.creationTimestamp;
|
res.CreationDate = data.metadata.creationTimestamp;
|
||||||
res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined);
|
res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined);
|
||||||
|
res.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, data);
|
||||||
|
|
||||||
const limits = {
|
const limits = {
|
||||||
Cpu: 0,
|
Cpu: 0,
|
||||||
|
@ -212,9 +213,9 @@ class KubernetesApplicationConverter {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static apiDeploymentToApplication(data, service, ingresses) {
|
static apiDeploymentToApplication(data, pods, service, ingresses) {
|
||||||
const res = new KubernetesApplication();
|
const res = new KubernetesApplication();
|
||||||
KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses);
|
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
|
||||||
res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT;
|
res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT;
|
||||||
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
|
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
|
||||||
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
|
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
|
||||||
|
@ -223,9 +224,9 @@ class KubernetesApplicationConverter {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static apiDaemonSetToApplication(data, service, ingresses) {
|
static apiDaemonSetToApplication(data, pods, service, ingresses) {
|
||||||
const res = new KubernetesApplication();
|
const res = new KubernetesApplication();
|
||||||
KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses);
|
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
|
||||||
res.ApplicationType = KubernetesApplicationTypes.DAEMONSET;
|
res.ApplicationType = KubernetesApplicationTypes.DAEMONSET;
|
||||||
res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL;
|
res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL;
|
||||||
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
|
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
|
||||||
|
@ -234,9 +235,9 @@ class KubernetesApplicationConverter {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static apiStatefulSetToapplication(data, service, ingresses) {
|
static apiStatefulSetToapplication(data, pods, service, ingresses) {
|
||||||
const res = new KubernetesApplication();
|
const res = new KubernetesApplication();
|
||||||
KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses);
|
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
|
||||||
res.ApplicationType = KubernetesApplicationTypes.STATEFULSET;
|
res.ApplicationType = KubernetesApplicationTypes.STATEFULSET;
|
||||||
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
|
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
|
||||||
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;
|
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;
|
||||||
|
@ -246,7 +247,7 @@ class KubernetesApplicationConverter {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims) {
|
static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims, nodesLabels) {
|
||||||
const res = new KubernetesApplicationFormValues();
|
const res = new KubernetesApplicationFormValues();
|
||||||
res.ApplicationType = app.ApplicationType;
|
res.ApplicationType = app.ApplicationType;
|
||||||
res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]);
|
res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]);
|
||||||
|
@ -276,6 +277,8 @@ class KubernetesApplicationConverter {
|
||||||
} else {
|
} else {
|
||||||
res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
|
res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity, nodesLabels);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ class KubernetesDaemonSetConverter {
|
||||||
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
|
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
|
||||||
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
|
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
|
||||||
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
|
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
|
||||||
|
KubernetesApplicationHelper.generateAffinityFromPlacements(res, formValues);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +52,7 @@ class KubernetesDaemonSetConverter {
|
||||||
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName;
|
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName;
|
||||||
payload.spec.template.spec.containers[0].name = daemonSet.Name;
|
payload.spec.template.spec.containers[0].name = daemonSet.Name;
|
||||||
payload.spec.template.spec.containers[0].image = daemonSet.Image;
|
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].env', daemonSet.Env);
|
||||||
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', daemonSet.VolumeMounts);
|
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', daemonSet.VolumeMounts);
|
||||||
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', daemonSet.Volumes);
|
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', daemonSet.Volumes);
|
||||||
|
|
|
@ -32,6 +32,7 @@ class KubernetesDeploymentConverter {
|
||||||
res.Containers = formValues.Containers;
|
res.Containers = formValues.Containers;
|
||||||
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
|
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
|
||||||
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
|
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
|
||||||
|
KubernetesApplicationHelper.generateAffinityFromPlacements(res, formValues);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +54,7 @@ class KubernetesDeploymentConverter {
|
||||||
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName;
|
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName;
|
||||||
payload.spec.template.spec.containers[0].name = deployment.Name;
|
payload.spec.template.spec.containers[0].name = deployment.Name;
|
||||||
payload.spec.template.spec.containers[0].image = deployment.Image;
|
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].env', deployment.Env);
|
||||||
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', deployment.VolumeMounts);
|
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', deployment.VolumeMounts);
|
||||||
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', deployment.Volumes);
|
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', deployment.Volumes);
|
||||||
|
|
|
@ -33,6 +33,7 @@ class KubernetesStatefulSetConverter {
|
||||||
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
|
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
|
||||||
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
|
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
|
||||||
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
|
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
|
||||||
|
KubernetesApplicationHelper.generateAffinityFromPlacements(res, formValues);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +57,7 @@ class KubernetesStatefulSetConverter {
|
||||||
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName;
|
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName;
|
||||||
payload.spec.template.spec.containers[0].name = statefulSet.Name;
|
payload.spec.template.spec.containers[0].name = statefulSet.Name;
|
||||||
payload.spec.template.spec.containers[0].image = statefulSet.Image;
|
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].env', statefulSet.Env);
|
||||||
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', statefulSet.VolumeMounts);
|
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', statefulSet.VolumeMounts);
|
||||||
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', statefulSet.Volumes);
|
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', statefulSet.Volumes);
|
||||||
|
|
|
@ -119,4 +119,15 @@ angular
|
||||||
}
|
}
|
||||||
return '';
|
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;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models';
|
import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models';
|
||||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||||
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||||
|
@ -10,6 +10,7 @@ import {
|
||||||
KubernetesApplicationPersistedFolderFormValue,
|
KubernetesApplicationPersistedFolderFormValue,
|
||||||
KubernetesApplicationPublishedPortFormValue,
|
KubernetesApplicationPublishedPortFormValue,
|
||||||
KubernetesApplicationAutoScalerFormValue,
|
KubernetesApplicationAutoScalerFormValue,
|
||||||
|
KubernetesApplicationPlacementFormValue,
|
||||||
} from 'Kubernetes/models/application/formValues';
|
} from 'Kubernetes/models/application/formValues';
|
||||||
import {
|
import {
|
||||||
KubernetesApplicationEnvConfigMapPayload,
|
KubernetesApplicationEnvConfigMapPayload,
|
||||||
|
@ -22,8 +23,21 @@ import {
|
||||||
KubernetesApplicationVolumeSecretPayload,
|
KubernetesApplicationVolumeSecretPayload,
|
||||||
} from 'Kubernetes/models/application/payloads';
|
} from 'Kubernetes/models/application/payloads';
|
||||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
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 {
|
class KubernetesApplicationHelper {
|
||||||
|
/* #region UTILITY FUNCTIONS */
|
||||||
|
static isExternalApplication(application) {
|
||||||
|
return !application.ApplicationOwner;
|
||||||
|
}
|
||||||
|
|
||||||
static associatePodsAndApplication(pods, app) {
|
static associatePodsAndApplication(pods, app) {
|
||||||
return _.filter(pods, { Labels: app.spec.selector.matchLabels });
|
return _.filter(pods, { Labels: app.spec.selector.matchLabels });
|
||||||
}
|
}
|
||||||
|
@ -93,10 +107,9 @@ class KubernetesApplicationHelper {
|
||||||
);
|
);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
/**
|
/* #region ENV VARIABLES FV <> ENV */
|
||||||
* FORMVALUES TO APPLICATION FUNCTIONS
|
|
||||||
*/
|
|
||||||
static generateEnvFromEnvVariables(envVariables) {
|
static generateEnvFromEnvVariables(envVariables) {
|
||||||
_.remove(envVariables, (item) => item.NeedsDeletion);
|
_.remove(envVariables, (item) => item.NeedsDeletion);
|
||||||
const env = _.map(envVariables, (item) => {
|
const env = _.map(envVariables, (item) => {
|
||||||
|
@ -108,6 +121,75 @@ class KubernetesApplicationHelper {
|
||||||
return env;
|
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) {
|
static generateEnvOrVolumesFromConfigurations(app, configurations) {
|
||||||
let finalEnv = [];
|
let finalEnv = [];
|
||||||
let finalVolumes = [];
|
let finalVolumes = [];
|
||||||
|
@ -181,110 +263,9 @@ class KubernetesApplicationHelper {
|
||||||
app.VolumeMounts = _.concat(app.VolumeMounts, finalMounts);
|
app.VolumeMounts = _.concat(app.VolumeMounts, finalMounts);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
static generateVolumesFromPersistentVolumClaims(app, volumeClaims) {
|
/* #region PUBLISHED PORTS FV <> PUBLISHED PORTS */
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) {
|
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) {
|
||||||
const generatePort = (port, rule) => {
|
const generatePort = (port, rule) => {
|
||||||
const res = new KubernetesApplicationPublishedPortFormValue();
|
const res = new KubernetesApplicationPublishedPortFormValue();
|
||||||
|
@ -313,7 +294,9 @@ class KubernetesApplicationHelper {
|
||||||
});
|
});
|
||||||
return finalRes;
|
return finalRes;
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region AUTOSCALER FV <> HORIZONTAL POD AUTOSCALER */
|
||||||
static generateAutoScalerFormValueFromHorizontalPodAutoScaler(autoScaler, replicasCount) {
|
static generateAutoScalerFormValueFromHorizontalPodAutoScaler(autoScaler, replicasCount) {
|
||||||
const res = new KubernetesApplicationAutoScalerFormValue();
|
const res = new KubernetesApplicationAutoScalerFormValue();
|
||||||
if (autoScaler) {
|
if (autoScaler) {
|
||||||
|
@ -330,12 +313,111 @@ class KubernetesApplicationHelper {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/* #endregion */
|
||||||
* !APPLICATION TO FORMVALUES FUNCTIONS
|
|
||||||
*/
|
|
||||||
|
|
||||||
static isExternalApplication(application) {
|
/* #region PERSISTED FOLDERS FV <> VOLUMES */
|
||||||
return !application.ApplicationOwner;
|
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;
|
export default KubernetesApplicationHelper;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes } from './models';
|
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes, KubernetesApplicationPlacementTypes } from './models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KubernetesApplicationFormValues Model
|
* KubernetesApplicationFormValues Model
|
||||||
|
@ -10,19 +10,21 @@ const _KubernetesApplicationFormValues = Object.freeze({
|
||||||
StackName: '',
|
StackName: '',
|
||||||
ApplicationOwner: '',
|
ApplicationOwner: '',
|
||||||
Image: '',
|
Image: '',
|
||||||
ReplicaCount: 1,
|
|
||||||
Note: '',
|
Note: '',
|
||||||
EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list
|
|
||||||
PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list
|
|
||||||
PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list
|
|
||||||
MemoryLimit: 0,
|
MemoryLimit: 0,
|
||||||
CpuLimit: 0,
|
CpuLimit: 0,
|
||||||
DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED,
|
DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED,
|
||||||
PublishingType: KubernetesApplicationPublishingTypes.INTERNAL,
|
ReplicaCount: 1,
|
||||||
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED,
|
|
||||||
Configurations: [], // KubernetesApplicationConfigurationFormValue list
|
|
||||||
Containers: [],
|
|
||||||
AutoScaler: {},
|
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,
|
OriginalIngresses: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -123,6 +125,15 @@ export function KubernetesApplicationPublishedPortFormValue() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function KubernetesApplicationPlacementFormValue() {
|
||||||
|
return {
|
||||||
|
Label: {},
|
||||||
|
Value: '',
|
||||||
|
NeedsDeletion: false,
|
||||||
|
IsNew: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KubernetesApplicationAutoScalerFormValue Model
|
* KubernetesApplicationAutoScalerFormValue Model
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -27,6 +27,11 @@ export const KubernetesApplicationPublishingTypes = Object.freeze({
|
||||||
INGRESS: 4,
|
INGRESS: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const KubernetesApplicationPlacementTypes = Object.freeze({
|
||||||
|
PREFERRED: 1,
|
||||||
|
MANDATORY: 2,
|
||||||
|
});
|
||||||
|
|
||||||
export const KubernetesApplicationQuotaDefaults = {
|
export const KubernetesApplicationQuotaDefaults = {
|
||||||
CpuLimit: 0.1,
|
CpuLimit: 0.1,
|
||||||
MemoryLimit: 64, // MB
|
MemoryLimit: 64, // MB
|
||||||
|
|
|
@ -15,6 +15,7 @@ const _KubernetesDaemonSet = Object.freeze({
|
||||||
ApplicationName: '',
|
ApplicationName: '',
|
||||||
ApplicationOwner: '',
|
ApplicationOwner: '',
|
||||||
Note: '',
|
Note: '',
|
||||||
|
Affinity: undefined, // KubernetesPodAffinity
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesDaemonSet {
|
export class KubernetesDaemonSet {
|
||||||
|
|
|
@ -16,6 +16,7 @@ const _KubernetesDeployment = Object.freeze({
|
||||||
ApplicationName: '',
|
ApplicationName: '',
|
||||||
ApplicationOwner: '',
|
ApplicationOwner: '',
|
||||||
Note: '',
|
Note: '',
|
||||||
|
Affinity: undefined, // KubernetesPodAffinity
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesDeployment {
|
export class KubernetesDeployment {
|
||||||
|
|
|
@ -26,6 +26,7 @@ const _KubernetesDeploymentCreatePayload = Object.freeze({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
|
affinity: {},
|
||||||
containers: [
|
containers: [
|
||||||
{
|
{
|
||||||
name: '',
|
name: '',
|
||||||
|
|
|
@ -18,6 +18,7 @@ const _KubernetesStatefulSet = Object.freeze({
|
||||||
ApplicationName: '',
|
ApplicationName: '',
|
||||||
ApplicationOwner: '',
|
ApplicationOwner: '',
|
||||||
Note: '',
|
Note: '',
|
||||||
|
Affinity: undefined, // KubernetesPodAffinity
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesStatefulSet {
|
export class KubernetesStatefulSet {
|
||||||
|
|
|
@ -20,4 +20,15 @@ export class KubernetesNodeHelper {
|
||||||
return label;
|
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')) };
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ function computeContainerStatus(statuses, name) {
|
||||||
function computeAffinity(affinity) {
|
function computeAffinity(affinity) {
|
||||||
const res = new KubernetesPodAffinity();
|
const res = new KubernetesPodAffinity();
|
||||||
if (affinity) {
|
if (affinity) {
|
||||||
res.NodeAffinity = affinity.nodeAffinity || {};
|
res.nodeAffinity = affinity.nodeAffinity || {};
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,56 +10,13 @@ export const KubernetesPodNodeAffinityNodeSelectorRequirementOperators = Object.
|
||||||
/**
|
/**
|
||||||
* KubernetesPodAffinity Model
|
* KubernetesPodAffinity Model
|
||||||
*/
|
*/
|
||||||
const _KubernetesPodAffinity = Object.freeze({
|
// this model will contain non transformed data (raw payload data)
|
||||||
NodeAffinity: {},
|
// either during creation flow (model > api)
|
||||||
// PodAffinity: {},
|
// than during reading flow (api > model)
|
||||||
// PodAntiAffinity: {},
|
export function KubernetesPodAffinity() {
|
||||||
});
|
return {
|
||||||
|
nodeAffinity: {}, // KubernetesPodNodeAffinityPayload
|
||||||
export class KubernetesPodAffinity {
|
// podAffinity: {},
|
||||||
constructor() {
|
// podAntiAffinity: {},
|
||||||
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)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ const _KubernetesPod = Object.freeze({
|
||||||
Labels: [],
|
Labels: [],
|
||||||
Affinity: {}, // KubernetesPodAffinity
|
Affinity: {}, // KubernetesPodAffinity
|
||||||
Tolerations: [], // KubernetesPodToleration[]
|
Tolerations: [], // KubernetesPodToleration[]
|
||||||
|
NodeSelector: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesPod {
|
export class KubernetesPod {
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
|
@ -111,10 +111,9 @@ class KubernetesApplicationService {
|
||||||
const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw);
|
const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw);
|
||||||
const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {};
|
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.Yaml = rootItem.value.Yaml;
|
||||||
application.Raw = rootItem.value.Raw;
|
application.Raw = rootItem.value.Raw;
|
||||||
application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods.value, application.Raw);
|
|
||||||
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
|
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
|
||||||
|
|
||||||
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application);
|
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application);
|
||||||
|
@ -143,8 +142,7 @@ class KubernetesApplicationService {
|
||||||
|
|
||||||
const convertToApplication = (item, converterFunc, services, pods, ingresses) => {
|
const convertToApplication = (item, converterFunc, services, pods, ingresses) => {
|
||||||
const service = KubernetesServiceHelper.findApplicationBoundService(services, item);
|
const service = KubernetesServiceHelper.findApplicationBoundService(services, item);
|
||||||
const application = converterFunc(item, service, ingresses);
|
const application = converterFunc(item, pods, service, ingresses);
|
||||||
application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item);
|
|
||||||
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
|
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
|
||||||
return application;
|
return application;
|
||||||
};
|
};
|
||||||
|
|
|
@ -926,6 +926,102 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #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">
|
<div class="col-sm-12 form-section-title">
|
||||||
Publishing the application
|
Publishing the application
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
KubernetesApplicationPublishingTypes,
|
KubernetesApplicationPublishingTypes,
|
||||||
KubernetesApplicationQuotaDefaults,
|
KubernetesApplicationQuotaDefaults,
|
||||||
KubernetesApplicationTypes,
|
KubernetesApplicationTypes,
|
||||||
|
KubernetesApplicationPlacementTypes,
|
||||||
} from 'Kubernetes/models/application/models';
|
} from 'Kubernetes/models/application/models';
|
||||||
import {
|
import {
|
||||||
KubernetesApplicationConfigurationFormValue,
|
KubernetesApplicationConfigurationFormValue,
|
||||||
|
@ -18,6 +19,7 @@ import {
|
||||||
KubernetesApplicationFormValues,
|
KubernetesApplicationFormValues,
|
||||||
KubernetesApplicationPersistedFolderFormValue,
|
KubernetesApplicationPersistedFolderFormValue,
|
||||||
KubernetesApplicationPublishedPortFormValue,
|
KubernetesApplicationPublishedPortFormValue,
|
||||||
|
KubernetesApplicationPlacementFormValue,
|
||||||
} from 'Kubernetes/models/application/formValues';
|
} from 'Kubernetes/models/application/formValues';
|
||||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||||
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
||||||
|
@ -25,6 +27,7 @@ import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceRese
|
||||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
|
import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
|
||||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||||
|
import { KubernetesNodeHelper } from 'Kubernetes/node/helper';
|
||||||
|
|
||||||
class KubernetesCreateApplicationController {
|
class KubernetesCreateApplicationController {
|
||||||
/* #region CONSTRUCTOR */
|
/* #region CONSTRUCTOR */
|
||||||
|
@ -66,6 +69,7 @@ class KubernetesCreateApplicationController {
|
||||||
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||||
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
||||||
this.ApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
|
this.ApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
|
||||||
|
this.ApplicationPlacementTypes = KubernetesApplicationPlacementTypes;
|
||||||
this.ApplicationTypes = KubernetesApplicationTypes;
|
this.ApplicationTypes = KubernetesApplicationTypes;
|
||||||
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
|
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
|
||||||
this.ServiceTypes = KubernetesServiceTypes;
|
this.ServiceTypes = KubernetesServiceTypes;
|
||||||
|
@ -248,6 +252,33 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #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 */
|
/* #region PUBLISHED PORTS UI MANAGEMENT */
|
||||||
addPublishedPort() {
|
addPublishedPort() {
|
||||||
const p = new KubernetesApplicationPublishedPortFormValue();
|
const p = new KubernetesApplicationPublishedPortFormValue();
|
||||||
|
@ -512,6 +543,15 @@ class KubernetesCreateApplicationController {
|
||||||
return this.formValues.PublishingType !== KubernetesApplicationPublishingTypes.INTERNAL && toKeepPorts.length === 0;
|
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() {
|
isNonScalable() {
|
||||||
const scalable = this.supportScalableReplicaDeployment();
|
const scalable = this.supportScalableReplicaDeployment();
|
||||||
const global = this.supportGlobalDeployment();
|
const global = this.supportGlobalDeployment();
|
||||||
|
@ -861,13 +901,20 @@ class KubernetesCreateApplicationController {
|
||||||
this.state.nodes.memory += filesizeParser(item.Memory);
|
this.state.nodes.memory += filesizeParser(item.Memory);
|
||||||
this.state.nodes.cpu += item.CPU;
|
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;
|
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
|
||||||
await this.refreshNamespaceData(namespace);
|
await this.refreshNamespaceData(namespace);
|
||||||
|
|
||||||
if (this.state.isEdit) {
|
if (this.state.isEdit) {
|
||||||
await this.getApplication();
|
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.formValues.OriginalIngresses = this.filteredIngresses;
|
||||||
this.savedFormValues = angular.copy(this.formValues);
|
this.savedFormValues = angular.copy(this.formValues);
|
||||||
delete this.formValues.ApplicationType;
|
delete this.formValues.ApplicationType;
|
||||||
|
|
|
@ -53,8 +53,8 @@ function computeAffinities(nodes, application) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pod.Affinity.NodeAffinity.requiredDuringSchedulingIgnoredDuringExecution) {
|
if (pod.Affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution) {
|
||||||
const unmatchedTerms = _.map(pod.Affinity.NodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, (t) => {
|
const unmatchedTerms = _.map(pod.Affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, (t) => {
|
||||||
const unmatchedExpressions = _.map(t.matchExpressions, (e) => {
|
const unmatchedExpressions = _.map(t.matchExpressions, (e) => {
|
||||||
const exists = {}.hasOwnProperty.call(n.Labels, e.key);
|
const exists = {}.hasOwnProperty.call(n.Labels, e.key);
|
||||||
const isIn = exists && _.includes(e.values, n.Labels[e.key]);
|
const isIn = exists && _.includes(e.values, n.Labels[e.key]);
|
||||||
|
|
|
@ -94,7 +94,7 @@
|
||||||
<!-- ADMIN + UNMET TAINTS -->
|
<!-- 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' }">
|
<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">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- !ADMIN + UNMET TAINTS -->
|
<!-- !ADMIN + UNMET TAINTS -->
|
||||||
|
|
Loading…
Reference in New Issue