mirror of https://github.com/portainer/portainer
feat(k8s/application): expose tolerations and affinities (#4063)
* feat(k8s/application): expose placement conditions * feat(k8s/applications): minor UI update * feat(k8s/application): update message for admin and non admin users * feat(kubernetes/applications): minor UI update Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>pull/4128/head
parent
63bf654d8d
commit
4431d748c2
|
@ -599,10 +599,6 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-select {
|
.small-select {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0px 6px;
|
padding: 0px 6px;
|
||||||
|
@ -618,17 +614,26 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||||
margin-left: 21px;
|
margin-left: 21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* switch box */
|
||||||
|
:root {
|
||||||
|
--switch-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.switch i,
|
.switch i,
|
||||||
.bootbox-form .checkbox i {
|
.bootbox-form .checkbox i {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding-right: 24px;
|
padding-right: var(--switch-size);
|
||||||
transition: all ease 0.2s;
|
transition: all ease 0.2s;
|
||||||
-webkit-transition: all ease 0.2s;
|
-webkit-transition: all ease 0.2s;
|
||||||
-moz-transition: all ease 0.2s;
|
-moz-transition: all ease 0.2s;
|
||||||
-o-transition: all ease 0.2s;
|
-o-transition: all ease 0.2s;
|
||||||
border-radius: 24px;
|
border-radius: var(--switch-size);
|
||||||
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5);
|
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -636,9 +641,9 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||||
.bootbox-form .checkbox i:before {
|
.bootbox-form .checkbox i:before {
|
||||||
display: block;
|
display: block;
|
||||||
content: '';
|
content: '';
|
||||||
width: 24px;
|
width: var(--switch-size);
|
||||||
height: 24px;
|
height: var(--switch-size);
|
||||||
border-radius: 24px;
|
border-radius: var(--switch-size);
|
||||||
background: white;
|
background: white;
|
||||||
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
@ -646,11 +651,19 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||||
.switch :checked + i,
|
.switch :checked + i,
|
||||||
.bootbox-form .checkbox :checked ~ i {
|
.bootbox-form .checkbox :checked ~ i {
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
padding-left: 24px;
|
padding-left: var(--switch-size);
|
||||||
-webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
-webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
||||||
-moz-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
-moz-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
||||||
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
|
||||||
}
|
}
|
||||||
|
/* !switch box */
|
||||||
|
|
||||||
|
/* small switch box */
|
||||||
|
.switch.small {
|
||||||
|
--switch-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* !small switch box */
|
||||||
|
|
||||||
.boxselector_wrapper {
|
.boxselector_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -21,7 +21,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsStacksDatat
|
||||||
showSystem: false,
|
showSystem: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.onSettingsRepeaterChange = function () {
|
this.onSettingsShowSystemChange = function () {
|
||||||
DatatableService.setDataTableSettings(this.tableKey, this.settings);
|
DatatableService.setDataTableSettings(this.tableKey, this.settings);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import _ from 'lodash-es';
|
|
||||||
import { KubernetesPod } from 'Kubernetes/models/pod/models';
|
|
||||||
class KubernetesPodConverter {
|
|
||||||
static computeStatus(statuses) {
|
|
||||||
const containerStatuses = _.map(statuses, 'state');
|
|
||||||
const running = _.filter(containerStatuses, (s) => s.running).length;
|
|
||||||
const waiting = _.filter(containerStatuses, (s) => s.waiting).length;
|
|
||||||
if (waiting) {
|
|
||||||
return 'Waiting';
|
|
||||||
} else if (!running) {
|
|
||||||
return 'Terminated';
|
|
||||||
}
|
|
||||||
return 'Running';
|
|
||||||
}
|
|
||||||
|
|
||||||
static apiToPod(data) {
|
|
||||||
const res = new KubernetesPod();
|
|
||||||
res.Id = data.metadata.uid;
|
|
||||||
res.Name = data.metadata.name;
|
|
||||||
res.Namespace = data.metadata.namespace;
|
|
||||||
res.Images = _.map(data.spec.containers, 'image');
|
|
||||||
res.Status = KubernetesPodConverter.computeStatus(data.status.containerStatuses);
|
|
||||||
res.Restarts = _.sumBy(data.status.containerStatuses, 'restartCount');
|
|
||||||
res.Node = data.spec.nodeName;
|
|
||||||
res.CreationDate = data.status.startTime;
|
|
||||||
res.Containers = data.spec.containers;
|
|
||||||
res.Labels = data.metadata.labels;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KubernetesPodConverter;
|
|
|
@ -2,6 +2,7 @@ import _ from 'lodash-es';
|
||||||
import { KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models';
|
import { KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models';
|
||||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||||
import { KubernetesApplicationTypes, KubernetesApplicationTypeStrings } from 'Kubernetes/models/application/models';
|
import { KubernetesApplicationTypes, KubernetesApplicationTypeStrings } from 'Kubernetes/models/application/models';
|
||||||
|
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
|
||||||
|
|
||||||
angular
|
angular
|
||||||
.module('portainer.kubernetes')
|
.module('portainer.kubernetes')
|
||||||
|
@ -99,4 +100,23 @@ angular
|
||||||
return 'All the instances of this application are sharing the same data.';
|
return 'All the instances of this application are sharing the same data.';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
})
|
||||||
|
.filter('kubernetesApplicationConstraintNodeAffinityValue', function () {
|
||||||
|
'use strict';
|
||||||
|
return function (values, operator) {
|
||||||
|
if (operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN || operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN) {
|
||||||
|
return values;
|
||||||
|
} else if (
|
||||||
|
operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.EXISTS ||
|
||||||
|
operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
} else if (
|
||||||
|
operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN ||
|
||||||
|
operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN
|
||||||
|
) {
|
||||||
|
return values[0];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
export const KubernetesApplicationDeploymentTypes = Object.freeze({
|
||||||
|
REPLICATED: 1,
|
||||||
|
GLOBAL: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const KubernetesApplicationDataAccessPolicies = Object.freeze({
|
||||||
|
SHARED: 1,
|
||||||
|
ISOLATED: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const KubernetesApplicationTypes = Object.freeze({
|
||||||
|
DEPLOYMENT: 1,
|
||||||
|
DAEMONSET: 2,
|
||||||
|
STATEFULSET: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const KubernetesApplicationTypeStrings = Object.freeze({
|
||||||
|
DEPLOYMENT: 'Deployment',
|
||||||
|
DAEMONSET: 'DaemonSet',
|
||||||
|
STATEFULSET: 'StatefulSet',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const KubernetesApplicationPublishingTypes = Object.freeze({
|
||||||
|
INTERNAL: 1,
|
||||||
|
CLUSTER: 2,
|
||||||
|
LOAD_BALANCER: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const KubernetesApplicationQuotaDefaults = {
|
||||||
|
CpuLimit: 0.1,
|
||||||
|
MemoryLimit: 64, // MB
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack';
|
||||||
|
|
||||||
|
export const KubernetesPortainerApplicationNameLabel = 'io.portainer.kubernetes.application.name';
|
||||||
|
|
||||||
|
export const KubernetesPortainerApplicationOwnerLabel = 'io.portainer.kubernetes.application.owner';
|
||||||
|
|
||||||
|
export const KubernetesPortainerApplicationNote = 'io.portainer.kubernetes.application.note';
|
|
@ -1,43 +1,4 @@
|
||||||
export const KubernetesApplicationDeploymentTypes = Object.freeze({
|
export * from './constants';
|
||||||
REPLICATED: 1,
|
|
||||||
GLOBAL: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const KubernetesApplicationDataAccessPolicies = Object.freeze({
|
|
||||||
SHARED: 1,
|
|
||||||
ISOLATED: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const KubernetesApplicationTypes = Object.freeze({
|
|
||||||
DEPLOYMENT: 1,
|
|
||||||
DAEMONSET: 2,
|
|
||||||
STATEFULSET: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const KubernetesApplicationTypeStrings = Object.freeze({
|
|
||||||
DEPLOYMENT: 'Deployment',
|
|
||||||
DAEMONSET: 'DaemonSet',
|
|
||||||
STATEFULSET: 'StatefulSet',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const KubernetesApplicationPublishingTypes = Object.freeze({
|
|
||||||
INTERNAL: 1,
|
|
||||||
CLUSTER: 2,
|
|
||||||
LOAD_BALANCER: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const KubernetesApplicationQuotaDefaults = {
|
|
||||||
CpuLimit: 0.1,
|
|
||||||
MemoryLimit: 64, // MB
|
|
||||||
};
|
|
||||||
|
|
||||||
export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack';
|
|
||||||
|
|
||||||
export const KubernetesPortainerApplicationNameLabel = 'io.portainer.kubernetes.application.name';
|
|
||||||
|
|
||||||
export const KubernetesPortainerApplicationOwnerLabel = 'io.portainer.kubernetes.application.owner';
|
|
||||||
|
|
||||||
export const KubernetesPortainerApplicationNote = 'io.portainer.kubernetes.application.note';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KubernetesApplication Model (Composite)
|
* KubernetesApplication Model (Composite)
|
|
@ -1,21 +0,0 @@
|
||||||
/**
|
|
||||||
* KubernetesPod Model
|
|
||||||
*/
|
|
||||||
const _KubernetesPod = Object.freeze({
|
|
||||||
Id: '',
|
|
||||||
Name: '',
|
|
||||||
Namespace: '',
|
|
||||||
Images: [],
|
|
||||||
Status: '',
|
|
||||||
Restarts: 0,
|
|
||||||
Node: '',
|
|
||||||
CreationDate: '',
|
|
||||||
Containers: [],
|
|
||||||
Labels: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
export class KubernetesPod {
|
|
||||||
constructor() {
|
|
||||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPod)));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
import { KubernetesNode, KubernetesNodeDetails } from 'Kubernetes/models/node/models';
|
import { KubernetesNode, KubernetesNodeDetails } from 'Kubernetes/node/models';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
|
|
||||||
class KubernetesNodeConverter {
|
class KubernetesNodeConverter {
|
||||||
|
@ -11,6 +11,7 @@ class KubernetesNodeConverter {
|
||||||
res.Id = data.metadata.uid;
|
res.Id = data.metadata.uid;
|
||||||
const hostName = _.find(data.status.addresses, { type: 'Hostname' });
|
const hostName = _.find(data.status.addresses, { type: 'Hostname' });
|
||||||
res.Name = hostName ? hostName.address : data.metadata.Name;
|
res.Name = hostName ? hostName.address : data.metadata.Name;
|
||||||
|
res.Labels = data.metadata.labels;
|
||||||
res.Role = _.has(data.metadata.labels, 'node-role.kubernetes.io/master') ? 'Master' : 'Worker';
|
res.Role = _.has(data.metadata.labels, 'node-role.kubernetes.io/master') ? 'Master' : 'Worker';
|
||||||
|
|
||||||
const ready = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.READY });
|
const ready = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.READY });
|
||||||
|
@ -39,6 +40,7 @@ class KubernetesNodeConverter {
|
||||||
res.Version = data.status.nodeInfo.kubeletVersion;
|
res.Version = data.status.nodeInfo.kubeletVersion;
|
||||||
const internalIP = _.find(data.status.addresses, { type: 'InternalIP' });
|
const internalIP = _.find(data.status.addresses, { type: 'InternalIP' });
|
||||||
res.IPAddress = internalIP ? internalIP.address : '-';
|
res.IPAddress = internalIP ? internalIP.address : '-';
|
||||||
|
res.Taints = data.spec.taints ? data.spec.taints : [];
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
const _KubernetesNode = Object.freeze({
|
const _KubernetesNode = Object.freeze({
|
||||||
Id: '',
|
Id: '',
|
||||||
Name: '',
|
Name: '',
|
||||||
|
Labels: {},
|
||||||
Role: '',
|
Role: '',
|
||||||
Status: '',
|
Status: '',
|
||||||
CPU: 0,
|
CPU: 0,
|
||||||
Memory: '',
|
Memory: '',
|
||||||
Version: '',
|
Version: '',
|
||||||
IPAddress: '',
|
IPAddress: '',
|
||||||
|
Taints: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesNode {
|
export class KubernetesNode {
|
|
@ -2,7 +2,7 @@ import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
import PortainerError from 'Portainer/error';
|
import PortainerError from 'Portainer/error';
|
||||||
import KubernetesNodeConverter from 'Kubernetes/converters/node';
|
import KubernetesNodeConverter from 'Kubernetes/node/converter';
|
||||||
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
||||||
|
|
||||||
class KubernetesNodeService {
|
class KubernetesNodeService {
|
|
@ -0,0 +1,54 @@
|
||||||
|
import _ from 'lodash-es';
|
||||||
|
import { KubernetesPod, KubernetesPodToleration, KubernetesPodAffinity } from 'Kubernetes/pod/models';
|
||||||
|
|
||||||
|
function computeStatus(statuses) {
|
||||||
|
const containerStatuses = _.map(statuses, 'state');
|
||||||
|
const running = _.filter(containerStatuses, (s) => s.running).length;
|
||||||
|
const waiting = _.filter(containerStatuses, (s) => s.waiting).length;
|
||||||
|
if (waiting) {
|
||||||
|
return 'Waiting';
|
||||||
|
} else if (!running) {
|
||||||
|
return 'Terminated';
|
||||||
|
}
|
||||||
|
return 'Running';
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeAffinity(affinity) {
|
||||||
|
const res = new KubernetesPodAffinity();
|
||||||
|
if (affinity) {
|
||||||
|
res.NodeAffinity = affinity.nodeAffinity || {};
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeTolerations(tolerations) {
|
||||||
|
return _.map(tolerations, (item) => {
|
||||||
|
const res = new KubernetesPodToleration();
|
||||||
|
res.Key = item.key;
|
||||||
|
res.Operator = item.operator;
|
||||||
|
res.Value = item.value;
|
||||||
|
res.TolerationSeconds = item.tolerationSeconds;
|
||||||
|
res.Effect = item.effect;
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class KubernetesPodConverter {
|
||||||
|
static apiToModel(data) {
|
||||||
|
const res = new KubernetesPod();
|
||||||
|
res.Id = data.metadata.uid;
|
||||||
|
res.Name = data.metadata.name;
|
||||||
|
res.Namespace = data.metadata.namespace;
|
||||||
|
res.Images = _.map(data.spec.containers, 'image');
|
||||||
|
res.Status = computeStatus(data.status.containerStatuses);
|
||||||
|
res.Restarts = _.sumBy(data.status.containerStatuses, 'restartCount');
|
||||||
|
res.Node = data.spec.nodeName;
|
||||||
|
res.CreationDate = data.status.startTime;
|
||||||
|
res.Containers = data.spec.containers;
|
||||||
|
res.Labels = data.metadata.labels;
|
||||||
|
res.Affinity = computeAffinity(data.spec.affinity);
|
||||||
|
res.NodeSelector = data.spec.nodeSelector;
|
||||||
|
res.Tolerations = computeTolerations(data.spec.tolerations);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
export const KubernetesPodNodeAffinityNodeSelectorRequirementOperators = Object.freeze({
|
||||||
|
IN: 'In',
|
||||||
|
NOT_IN: 'NotIn',
|
||||||
|
EXISTS: 'Exists',
|
||||||
|
DOES_NOT_EXIST: 'DoesNotExist',
|
||||||
|
GREATER_THAN: 'Gt',
|
||||||
|
LOWER_THAN: 'Lt',
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
export * from './affinities';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KubernetesPod Model
|
||||||
|
*/
|
||||||
|
const _KubernetesPod = Object.freeze({
|
||||||
|
Id: '',
|
||||||
|
Name: '',
|
||||||
|
Namespace: '',
|
||||||
|
Images: [],
|
||||||
|
Status: '',
|
||||||
|
Restarts: 0,
|
||||||
|
Node: '',
|
||||||
|
CreationDate: '',
|
||||||
|
Containers: [],
|
||||||
|
Labels: [],
|
||||||
|
Affinity: {}, // KubernetesPodAffinity
|
||||||
|
Tolerations: [], // KubernetesPodToleration[]
|
||||||
|
});
|
||||||
|
|
||||||
|
export class KubernetesPod {
|
||||||
|
constructor() {
|
||||||
|
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPod)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KubernetesPodToleration Model
|
||||||
|
*/
|
||||||
|
const _KubernetesPodToleration = Object.freeze({
|
||||||
|
Key: '',
|
||||||
|
Operator: '',
|
||||||
|
Value: '',
|
||||||
|
TolerationSeconds: 0,
|
||||||
|
Effect: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
export class KubernetesPodToleration {
|
||||||
|
constructor() {
|
||||||
|
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodToleration)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import angular from 'angular';
|
||||||
import PortainerError from 'Portainer/error';
|
import PortainerError from 'Portainer/error';
|
||||||
|
|
||||||
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
||||||
import KubernetesPodConverter from 'Kubernetes/converters/pod';
|
import KubernetesPodConverter from 'Kubernetes/pod/converter';
|
||||||
|
|
||||||
class KubernetesPodService {
|
class KubernetesPodService {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -21,7 +21,7 @@ class KubernetesPodService {
|
||||||
async getAllAsync(namespace) {
|
async getAllAsync(namespace) {
|
||||||
try {
|
try {
|
||||||
const data = await this.KubernetesPods(namespace).get().$promise;
|
const data = await this.KubernetesPods(namespace).get().$promise;
|
||||||
return _.map(data.items, (item) => KubernetesPodConverter.apiToPod(item));
|
return _.map(data.items, (item) => KubernetesPodConverter.apiToModel(item));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new PortainerError('Unable to retrieve pods', err);
|
throw new PortainerError('Unable to retrieve pods', err);
|
||||||
}
|
}
|
|
@ -128,6 +128,24 @@
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
|
|
||||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||||
|
<uib-tab-heading> <i class="fas fa-compress-arrows-alt space-right" aria-hidden="true"></i> Placement </uib-tab-heading>
|
||||||
|
<div class="small text-muted" style="padding: 20px;">
|
||||||
|
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
The placement component helps you understand whether or not this application can be deployed on a specific node.
|
||||||
|
</div>
|
||||||
|
<kubernetes-application-placements-datatable
|
||||||
|
title-text="Placement constraints/preferences"
|
||||||
|
title-icon="fa-compress-arrows-alt"
|
||||||
|
dataset="ctrl.placements"
|
||||||
|
table-key="kubernetes.application.placements"
|
||||||
|
order-by="Name"
|
||||||
|
reverse-order="false"
|
||||||
|
loading="ctrl.state.dataLoading"
|
||||||
|
refresh-callback="ctrl.getApplication"
|
||||||
|
></kubernetes-application-placements-datatable>
|
||||||
|
</uib-tab>
|
||||||
|
|
||||||
|
<uib-tab index="2" classes="btn-sm" select="ctrl.selectTab(2)">
|
||||||
<uib-tab-heading>
|
<uib-tab-heading>
|
||||||
<i class="fa fa-history space-right" aria-hidden="true"></i> Events
|
<i class="fa fa-history space-right" aria-hidden="true"></i> Events
|
||||||
<div ng-if="ctrl.hasEventWarnings()">
|
<div ng-if="ctrl.hasEventWarnings()">
|
||||||
|
@ -147,7 +165,7 @@
|
||||||
></kubernetes-events-datatable>
|
></kubernetes-events-datatable>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
|
|
||||||
<uib-tab index="2" ng-if="ctrl.application.Yaml" select="ctrl.showEditor()" classes="btn-sm">
|
<uib-tab index="3" ng-if="ctrl.application.Yaml" select="ctrl.showEditor()" classes="btn-sm">
|
||||||
<uib-tab-heading> <i class="fa fa-code space-right" aria-hidden="true"></i> YAML </uib-tab-heading>
|
<uib-tab-heading> <i class="fa fa-code space-right" aria-hidden="true"></i> YAML </uib-tab-heading>
|
||||||
<div style="padding-right: 25px;" ng-if="ctrl.state.showEditorTab">
|
<div style="padding-right: 25px;" ng-if="ctrl.state.showEditorTab">
|
||||||
<kubernetes-yaml-inspector key="application-yaml" data="ctrl.application.Yaml"></kubernetes-yaml-inspector>
|
<kubernetes-yaml-inspector key="application-yaml" data="ctrl.application.Yaml"></kubernetes-yaml-inspector>
|
||||||
|
|
|
@ -1,9 +1,91 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
|
import * as JsonPatch from 'fast-json-patch';
|
||||||
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
|
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
|
||||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||||
|
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
|
||||||
|
|
||||||
|
function computeTolerations(nodes, application) {
|
||||||
|
const pod = application.Pods[0];
|
||||||
|
_.forEach(nodes, (n) => {
|
||||||
|
n.AcceptsApplication = true;
|
||||||
|
n.Expanded = false;
|
||||||
|
if (!pod) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
n.UnmetTaints = [];
|
||||||
|
_.forEach(n.Taints, (t) => {
|
||||||
|
const matchKeyMatchValueMatchEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Equal', Value: t.value, Effect: t.effect });
|
||||||
|
const matchKeyAnyValueMatchEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Exists', Effect: t.effect });
|
||||||
|
const matchKeyMatchValueAnyEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Equal', Value: t.value, Effect: '' });
|
||||||
|
const matchKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Exists', Effect: '' });
|
||||||
|
const anyKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: '', Operator: 'Exists', Effect: '' });
|
||||||
|
|
||||||
|
if (!matchKeyMatchValueMatchEffect && !matchKeyAnyValueMatchEffect && !matchKeyMatchValueAnyEffect && !matchKeyAnyValueAnyEffect && !anyKeyAnyValueAnyEffect) {
|
||||||
|
n.AcceptsApplication = false;
|
||||||
|
n.UnmetTaints.push(t);
|
||||||
|
} else {
|
||||||
|
n.AcceptsApplication = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For node requirement format depending on operator value
|
||||||
|
// see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#nodeselectorrequirement-v1-core
|
||||||
|
// Some operators require empty "values" field, some only one element in "values" field, etc
|
||||||
|
|
||||||
|
function computeAffinities(nodes, application) {
|
||||||
|
const pod = application.Pods[0];
|
||||||
|
_.forEach(nodes, (n) => {
|
||||||
|
if (pod.NodeSelector) {
|
||||||
|
const patch = JsonPatch.compare(n.Labels, pod.NodeSelector);
|
||||||
|
_.remove(patch, { op: 'remove' });
|
||||||
|
n.UnmatchedNodeSelectorLabels = _.map(patch, (i) => {
|
||||||
|
return { key: _.trimStart(i.path, '/'), value: i.value };
|
||||||
|
});
|
||||||
|
if (n.UnmatchedNodeSelectorLabels.length) {
|
||||||
|
n.AcceptsApplication = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
if (
|
||||||
|
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.EXISTS && exists) ||
|
||||||
|
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST && !exists) ||
|
||||||
|
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN && isIn) ||
|
||||||
|
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN && !isIn) ||
|
||||||
|
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN && exists && parseInt(n.Labels[e.key]) > parseInt(e.values[0])) ||
|
||||||
|
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN && exists && parseInt(n.Labels[e.key]) < parseInt(e.values[0]))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
return _.without(unmatchedExpressions, undefined);
|
||||||
|
});
|
||||||
|
_.remove(unmatchedTerms, (i) => i.length === 0);
|
||||||
|
n.UnmatchedNodeAffinities = unmatchedTerms;
|
||||||
|
if (n.UnmatchedNodeAffinities.length) {
|
||||||
|
n.AcceptsApplication = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computePlacements(nodes, application) {
|
||||||
|
nodes = computeTolerations(nodes, application);
|
||||||
|
nodes = computeAffinities(nodes, application);
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
class KubernetesApplicationController {
|
class KubernetesApplicationController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -18,6 +100,7 @@ class KubernetesApplicationController {
|
||||||
KubernetesEventService,
|
KubernetesEventService,
|
||||||
KubernetesStackService,
|
KubernetesStackService,
|
||||||
KubernetesPodService,
|
KubernetesPodService,
|
||||||
|
KubernetesNodeService,
|
||||||
KubernetesNamespaceHelper
|
KubernetesNamespaceHelper
|
||||||
) {
|
) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
|
@ -31,6 +114,7 @@ class KubernetesApplicationController {
|
||||||
this.KubernetesEventService = KubernetesEventService;
|
this.KubernetesEventService = KubernetesEventService;
|
||||||
this.KubernetesStackService = KubernetesStackService;
|
this.KubernetesStackService = KubernetesStackService;
|
||||||
this.KubernetesPodService = KubernetesPodService;
|
this.KubernetesPodService = KubernetesPodService;
|
||||||
|
this.KubernetesNodeService = KubernetesNodeService;
|
||||||
|
|
||||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||||
|
|
||||||
|
@ -103,7 +187,6 @@ class KubernetesApplicationController {
|
||||||
/**
|
/**
|
||||||
* ROLLBACK
|
* ROLLBACK
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async rollbackApplicationAsync() {
|
async rollbackApplicationAsync() {
|
||||||
try {
|
try {
|
||||||
// await this.KubernetesApplicationService.rollback(this.application, this.formValues.SelectedRevision);
|
// await this.KubernetesApplicationService.rollback(this.application, this.formValues.SelectedRevision);
|
||||||
|
@ -196,7 +279,11 @@ class KubernetesApplicationController {
|
||||||
async getApplicationAsync() {
|
async getApplicationAsync() {
|
||||||
try {
|
try {
|
||||||
this.state.dataLoading = true;
|
this.state.dataLoading = true;
|
||||||
this.application = await this.KubernetesApplicationService.get(this.state.params.namespace, this.state.params.name);
|
const [application, nodes] = await Promise.all([
|
||||||
|
this.KubernetesApplicationService.get(this.state.params.namespace, this.state.params.name),
|
||||||
|
this.KubernetesNodeService.get(),
|
||||||
|
]);
|
||||||
|
this.application = application;
|
||||||
this.formValues.Note = this.application.Note;
|
this.formValues.Note = this.application.Note;
|
||||||
if (this.application.Note) {
|
if (this.application.Note) {
|
||||||
this.state.expandedNote = true;
|
this.state.expandedNote = true;
|
||||||
|
@ -204,6 +291,8 @@ class KubernetesApplicationController {
|
||||||
if (this.application.CurrentRevision) {
|
if (this.application.CurrentRevision) {
|
||||||
this.formValues.SelectedRevision = _.find(this.application.Revisions, { revision: this.application.CurrentRevision.revision });
|
this.formValues.SelectedRevision = _.find(this.application.Revisions, { revision: this.application.CurrentRevision.revision });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.placements = computePlacements(nodes, this.application);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
import * as _ from 'lodash-es';
|
||||||
|
|
||||||
|
angular.module('portainer.docker').controller('KubernetesApplicationPlacementsDatatableController', function ($scope, $controller, DatatableService, Authentication) {
|
||||||
|
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||||
|
this.state = Object.assign(this.state, {
|
||||||
|
expandedItems: [],
|
||||||
|
expandAll: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.expandItem = function (item, expanded) {
|
||||||
|
if (!this.itemCanExpand(item)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Expanded = expanded;
|
||||||
|
if (!expanded) {
|
||||||
|
item.Highlighted = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.itemCanExpand = function (item) {
|
||||||
|
return !item.AcceptsApplication;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.hasExpandableItems = function () {
|
||||||
|
return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.expandAll = function () {
|
||||||
|
this.state.expandAll = !this.state.expandAll;
|
||||||
|
_.forEach(this.state.filteredDataSet, (item) => {
|
||||||
|
if (this.itemCanExpand(item)) {
|
||||||
|
this.expandItem(item, this.state.expandAll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$onInit = function () {
|
||||||
|
this.isAdmin = Authentication.isAdmin();
|
||||||
|
this.setDefaults();
|
||||||
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
|
this.state.orderBy = this.orderBy;
|
||||||
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
|
if (storedOrder !== null) {
|
||||||
|
this.state.reverseOrder = storedOrder.reverse;
|
||||||
|
this.state.orderBy = storedOrder.orderBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
|
if (textFilter !== null) {
|
||||||
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
|
if (storedFilters !== null) {
|
||||||
|
this.filters = storedFilters;
|
||||||
|
}
|
||||||
|
if (this.filters && this.filters.state) {
|
||||||
|
this.filters.state.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||||
|
if (storedSettings !== null) {
|
||||||
|
this.settings = storedSettings;
|
||||||
|
this.settings.open = false;
|
||||||
|
}
|
||||||
|
this.onSettingsRepeaterChange();
|
||||||
|
};
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
angular.module('portainer.kubernetes').component('kubernetesApplicationPlacementsDatatable', {
|
||||||
|
templateUrl: './template.html',
|
||||||
|
controller: 'KubernetesApplicationPlacementsDatatableController',
|
||||||
|
bindings: {
|
||||||
|
titleText: '@',
|
||||||
|
titleIcon: '@',
|
||||||
|
dataset: '<',
|
||||||
|
tableKey: '@',
|
||||||
|
orderBy: '@',
|
||||||
|
reverseOrder: '<',
|
||||||
|
refreshCallback: '<',
|
||||||
|
loading: '<',
|
||||||
|
removeAction: '<',
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,184 @@
|
||||||
|
<div class="datatable">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body classes="no-padding">
|
||||||
|
<div class="toolBar">
|
||||||
|
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||||
|
<div class="settings">
|
||||||
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
|
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
|
<div class="tableMenu">
|
||||||
|
<div class="menuHeader">
|
||||||
|
Table settings
|
||||||
|
</div>
|
||||||
|
<div class="menuContent">
|
||||||
|
<div>
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
|
||||||
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
|
<label for="settings_refresh_rate">
|
||||||
|
Refresh rate
|
||||||
|
</label>
|
||||||
|
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
<option value="60">1min</option>
|
||||||
|
<option value="120">2min</option>
|
||||||
|
<option value="300">5min</option>
|
||||||
|
</select>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="searchBar">
|
||||||
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="searchInput"
|
||||||
|
ng-model="$ctrl.state.textFilter"
|
||||||
|
ng-change="$ctrl.onTextFilterChange()"
|
||||||
|
placeholder="Search..."
|
||||||
|
auto-focus
|
||||||
|
ng-model-options="{ debounce: 300 }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover nowrap-cells">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 2%;">
|
||||||
|
<a ng-click="$ctrl.expandAll()" ng-if="$ctrl.hasExpandableItems()">
|
||||||
|
<i ng-class="{ 'fas fa-angle-down': $ctrl.state.expandAll, 'fas fa-angle-right': !$ctrl.state.expandAll }" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th style="width: 98%;">
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||||
|
Node
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter: $ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
|
||||||
|
ng-class="{ active: item.Checked }"
|
||||||
|
ng-style="{ background: item.Highlighted ? '#d5e8f3' : '' }"
|
||||||
|
ng-click="$ctrl.expandItem(item, !item.Expanded)"
|
||||||
|
pagination-id="$ctrl.tableKey"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<a ng-if="$ctrl.itemCanExpand(item)">
|
||||||
|
<i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
<i ng-if="item.AcceptsApplication" class="fa fa-check green-icon" aria-hidden="true"></i>
|
||||||
|
<i ng-if="!item.AcceptsApplication" class="fa fa-times red-icon" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ item.Name }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- 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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- !ADMIN + UNMET TAINTS -->
|
||||||
|
<!-- USER + UNMET TAINTS -->
|
||||||
|
<tr ng-if="!$ctrl.isAdmin && item.UnmetTaints.length" ng-show="item.Expanded" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
||||||
|
<td colspan="2">
|
||||||
|
Placement constraint not respected for that node.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- ! USER + UNMET TAINTS -->
|
||||||
|
<!-- ADMIN + UNMET NODE SELECTOR LABELS -->
|
||||||
|
<tr
|
||||||
|
ng-if="$ctrl.isAdmin"
|
||||||
|
ng-show="item.Expanded"
|
||||||
|
ng-repeat="label in item.UnmatchedNodeSelectorLabels"
|
||||||
|
ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }"
|
||||||
|
>
|
||||||
|
<td colspan="2">
|
||||||
|
This application can only be scheduled on a node where the label <code>{{ label.key }}</code> is set to <code>{{ label.value }}</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- ! ADMIN + UNMET NODE SELECTOR LABELS -->
|
||||||
|
<!-- USER + UNMET NODE SELECTOR LABELS || UNMET NODE AFFINITIES -->
|
||||||
|
<tr
|
||||||
|
ng-if="!$ctrl.isAdmin && (item.UnmatchedNodeSelectorLabels.length || item.UnmatchedNodeAffinities.length)"
|
||||||
|
ng-show="item.Expanded"
|
||||||
|
ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }"
|
||||||
|
>
|
||||||
|
<td colspan="2">
|
||||||
|
Placement label not respected for that node.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- ! USER + UNMET NODE SELECTOR LABELS || UNMET NODE AFFINITIES -->
|
||||||
|
<!-- ADMIN + UNMET NODE AFFINITIES -->
|
||||||
|
<tr ng-if="$ctrl.isAdmin" ng-show="item.Expanded && item.UnmatchedNodeAffinities.length" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
||||||
|
<td colspan="2">
|
||||||
|
This application can only be scheduled on nodes respecting one of the following labels combination:
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
dir-paginate-end
|
||||||
|
ng-if="$ctrl.isAdmin"
|
||||||
|
ng-show="item.Expanded"
|
||||||
|
ng-repeat="aff in item.UnmatchedNodeAffinities"
|
||||||
|
ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }"
|
||||||
|
>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<code ng-repeat-start="term in aff track by $index">
|
||||||
|
{{ term.key }} {{ term.operator }} {{ term.values | kubernetesApplicationConstraintNodeAffinityValue: term.operator }}
|
||||||
|
</code>
|
||||||
|
<span ng-repeat-end>{{ $last ? '' : ' + ' }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- ! ADMIN + UNMET NODE AFFINITIES -->
|
||||||
|
<tr ng-if="$ctrl.loading">
|
||||||
|
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="!$ctrl.loading && (!$ctrl.dataset || $ctrl.state.filteredDataSet.length === 0)">
|
||||||
|
<td colspan="2" class="text-center text-muted">No node available.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="footer" ng-if="$ctrl.dataset">
|
||||||
|
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
|
||||||
|
<div class="paginationControls">
|
||||||
|
<form class="form-inline">
|
||||||
|
<span class="limitSelector">
|
||||||
|
<span style="margin-right: 5px;">
|
||||||
|
Items per page
|
||||||
|
</span>
|
||||||
|
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||||
|
<option value="0">All</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<dir-pagination-controls max-size="5" pagination-id="$ctrl.tableKey"></dir-pagination-controls>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
Loading…
Reference in New Issue