diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index ff2ef43b0..af1fe6b38 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -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; } diff --git a/app/kubernetes/converters/daemonSet.js b/app/kubernetes/converters/daemonSet.js index fa74b6bf1..8360ca9a7 100644 --- a/app/kubernetes/converters/daemonSet.js +++ b/app/kubernetes/converters/daemonSet.js @@ -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); diff --git a/app/kubernetes/converters/deployment.js b/app/kubernetes/converters/deployment.js index dad947923..471faa179 100644 --- a/app/kubernetes/converters/deployment.js +++ b/app/kubernetes/converters/deployment.js @@ -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); diff --git a/app/kubernetes/converters/statefulSet.js b/app/kubernetes/converters/statefulSet.js index 7867af0ee..26cffd4ac 100644 --- a/app/kubernetes/converters/statefulSet.js +++ b/app/kubernetes/converters/statefulSet.js @@ -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); diff --git a/app/kubernetes/filters/applicationFilters.js b/app/kubernetes/filters/applicationFilters.js index b58e783cb..0629c12ff 100644 --- a/app/kubernetes/filters/applicationFilters.js +++ b/app/kubernetes/filters/applicationFilters.js @@ -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; + }; }); diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index 7a39186ca..8a1e75336 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -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; diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index 6d789c10b..4a8e6cb79 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -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 */ diff --git a/app/kubernetes/models/application/models/constants.js b/app/kubernetes/models/application/models/constants.js index 3be2fcdfb..2d42bba05 100644 --- a/app/kubernetes/models/application/models/constants.js +++ b/app/kubernetes/models/application/models/constants.js @@ -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 diff --git a/app/kubernetes/models/daemon-set/models.js b/app/kubernetes/models/daemon-set/models.js index ba0e4d6cd..a4c4fc411 100644 --- a/app/kubernetes/models/daemon-set/models.js +++ b/app/kubernetes/models/daemon-set/models.js @@ -15,6 +15,7 @@ const _KubernetesDaemonSet = Object.freeze({ ApplicationName: '', ApplicationOwner: '', Note: '', + Affinity: undefined, // KubernetesPodAffinity }); export class KubernetesDaemonSet { diff --git a/app/kubernetes/models/deployment/models.js b/app/kubernetes/models/deployment/models.js index 05161e929..df8a8d79c 100644 --- a/app/kubernetes/models/deployment/models.js +++ b/app/kubernetes/models/deployment/models.js @@ -16,6 +16,7 @@ const _KubernetesDeployment = Object.freeze({ ApplicationName: '', ApplicationOwner: '', Note: '', + Affinity: undefined, // KubernetesPodAffinity }); export class KubernetesDeployment { diff --git a/app/kubernetes/models/deployment/payloads.js b/app/kubernetes/models/deployment/payloads.js index 89047b08f..e3fff904b 100644 --- a/app/kubernetes/models/deployment/payloads.js +++ b/app/kubernetes/models/deployment/payloads.js @@ -26,6 +26,7 @@ const _KubernetesDeploymentCreatePayload = Object.freeze({ }, }, spec: { + affinity: {}, containers: [ { name: '', diff --git a/app/kubernetes/models/stateful-set/models.js b/app/kubernetes/models/stateful-set/models.js index 7520ec116..28609416c 100644 --- a/app/kubernetes/models/stateful-set/models.js +++ b/app/kubernetes/models/stateful-set/models.js @@ -18,6 +18,7 @@ const _KubernetesStatefulSet = Object.freeze({ ApplicationName: '', ApplicationOwner: '', Note: '', + Affinity: undefined, // KubernetesPodAffinity }); export class KubernetesStatefulSet { diff --git a/app/kubernetes/node/helper.js b/app/kubernetes/node/helper.js index 57eaa583f..73ae999f9 100644 --- a/app/kubernetes/node/helper.js +++ b/app/kubernetes/node/helper.js @@ -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')) }; + }); + } } diff --git a/app/kubernetes/pod/converter.js b/app/kubernetes/pod/converter.js index 3aae29482..78e2edb8a 100644 --- a/app/kubernetes/pod/converter.js +++ b/app/kubernetes/pod/converter.js @@ -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; } diff --git a/app/kubernetes/pod/models/affinities.js b/app/kubernetes/pod/models/affinities.js index e0bafb202..08b38f9d0 100644 --- a/app/kubernetes/pod/models/affinities.js +++ b/app/kubernetes/pod/models/affinities.js @@ -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: {}, + }; } diff --git a/app/kubernetes/pod/models/index.js b/app/kubernetes/pod/models/index.js index 3cf9488b7..b00307b62 100644 --- a/app/kubernetes/pod/models/index.js +++ b/app/kubernetes/pod/models/index.js @@ -16,6 +16,7 @@ const _KubernetesPod = Object.freeze({ Labels: [], Affinity: {}, // KubernetesPodAffinity Tolerations: [], // KubernetesPodToleration[] + NodeSelector: undefined, }); export class KubernetesPod { diff --git a/app/kubernetes/pod/payloads/affinities.js b/app/kubernetes/pod/payloads/affinities.js new file mode 100644 index 000000000..5517fab38 --- /dev/null +++ b/app/kubernetes/pod/payloads/affinities.js @@ -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 + }; +} diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index 27ce3e9d9..7be565781 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -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; }; diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index c6eb5f637..0bd5acbc5 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -926,6 +926,102 @@ +
{{ taint.Key }}{{ taint.Value ? '=' + taint.Value : '' }}:{{ taint.effect }}
+ This application is missing a toleration for the taint {{ taint.Key }}{{ taint.Value ? '=' + taint.Value : '' }}:{{ taint.Effect }}