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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.switch input {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.small-select {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  padding: 0px 6px;
 | 
			
		||||
| 
						 | 
				
			
			@ -618,17 +614,26 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
 | 
			
		|||
  margin-left: 21px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* switch box */
 | 
			
		||||
:root {
 | 
			
		||||
  --switch-size: 24px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.switch input {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.switch i,
 | 
			
		||||
.bootbox-form .checkbox i {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  vertical-align: middle;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  padding-right: 24px;
 | 
			
		||||
  padding-right: var(--switch-size);
 | 
			
		||||
  transition: all ease 0.2s;
 | 
			
		||||
  -webkit-transition: all ease 0.2s;
 | 
			
		||||
  -moz-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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -636,9 +641,9 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
 | 
			
		|||
.bootbox-form .checkbox i:before {
 | 
			
		||||
  display: block;
 | 
			
		||||
  content: '';
 | 
			
		||||
  width: 24px;
 | 
			
		||||
  height: 24px;
 | 
			
		||||
  border-radius: 24px;
 | 
			
		||||
  width: var(--switch-size);
 | 
			
		||||
  height: var(--switch-size);
 | 
			
		||||
  border-radius: var(--switch-size);
 | 
			
		||||
  background: white;
 | 
			
		||||
  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,
 | 
			
		||||
.bootbox-form .checkbox :checked ~ i {
 | 
			
		||||
  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;
 | 
			
		||||
  -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;
 | 
			
		||||
}
 | 
			
		||||
/* !switch box */
 | 
			
		||||
 | 
			
		||||
/* small switch box */
 | 
			
		||||
.switch.small {
 | 
			
		||||
  --switch-size: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* !small switch box */
 | 
			
		||||
 | 
			
		||||
.boxselector_wrapper {
 | 
			
		||||
  display: flex;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsStacksDatat
 | 
			
		|||
      showSystem: false,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.onSettingsRepeaterChange = function () {
 | 
			
		||||
    this.onSettingsShowSystemChange = function () {
 | 
			
		||||
      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 { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
 | 
			
		||||
import { KubernetesApplicationTypes, KubernetesApplicationTypeStrings } from 'Kubernetes/models/application/models';
 | 
			
		||||
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
 | 
			
		||||
 | 
			
		||||
angular
 | 
			
		||||
  .module('portainer.kubernetes')
 | 
			
		||||
| 
						 | 
				
			
			@ -99,4 +100,23 @@ angular
 | 
			
		|||
          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({
 | 
			
		||||
  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';
 | 
			
		||||
export * from './constants';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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 { KubernetesNode, KubernetesNodeDetails } from 'Kubernetes/models/node/models';
 | 
			
		||||
import { KubernetesNode, KubernetesNodeDetails } from 'Kubernetes/node/models';
 | 
			
		||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
 | 
			
		||||
 | 
			
		||||
class KubernetesNodeConverter {
 | 
			
		||||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ class KubernetesNodeConverter {
 | 
			
		|||
    res.Id = data.metadata.uid;
 | 
			
		||||
    const hostName = _.find(data.status.addresses, { type: 'Hostname' });
 | 
			
		||||
    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';
 | 
			
		||||
 | 
			
		||||
    const ready = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.READY });
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +40,7 @@ class KubernetesNodeConverter {
 | 
			
		|||
    res.Version = data.status.nodeInfo.kubeletVersion;
 | 
			
		||||
    const internalIP = _.find(data.status.addresses, { type: 'InternalIP' });
 | 
			
		||||
    res.IPAddress = internalIP ? internalIP.address : '-';
 | 
			
		||||
    res.Taints = data.spec.taints ? data.spec.taints : [];
 | 
			
		||||
    return res;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -4,12 +4,14 @@
 | 
			
		|||
const _KubernetesNode = Object.freeze({
 | 
			
		||||
  Id: '',
 | 
			
		||||
  Name: '',
 | 
			
		||||
  Labels: {},
 | 
			
		||||
  Role: '',
 | 
			
		||||
  Status: '',
 | 
			
		||||
  CPU: 0,
 | 
			
		||||
  Memory: '',
 | 
			
		||||
  Version: '',
 | 
			
		||||
  IPAddress: '',
 | 
			
		||||
  Taints: [],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export class KubernetesNode {
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import angular from 'angular';
 | 
			
		|||
import _ from 'lodash-es';
 | 
			
		||||
 | 
			
		||||
import PortainerError from 'Portainer/error';
 | 
			
		||||
import KubernetesNodeConverter from 'Kubernetes/converters/node';
 | 
			
		||||
import KubernetesNodeConverter from 'Kubernetes/node/converter';
 | 
			
		||||
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
 | 
			
		||||
 | 
			
		||||
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 { KubernetesCommonParams } from 'Kubernetes/models/common/params';
 | 
			
		||||
import KubernetesPodConverter from 'Kubernetes/converters/pod';
 | 
			
		||||
import KubernetesPodConverter from 'Kubernetes/pod/converter';
 | 
			
		||||
 | 
			
		||||
class KubernetesPodService {
 | 
			
		||||
  /* @ngInject */
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ class KubernetesPodService {
 | 
			
		|||
  async getAllAsync(namespace) {
 | 
			
		||||
    try {
 | 
			
		||||
      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) {
 | 
			
		||||
      throw new PortainerError('Unable to retrieve pods', err);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +128,24 @@
 | 
			
		|||
            </uib-tab>
 | 
			
		||||
 | 
			
		||||
            <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>
 | 
			
		||||
                <i class="fa fa-history space-right" aria-hidden="true"></i> Events
 | 
			
		||||
                <div ng-if="ctrl.hasEventWarnings()">
 | 
			
		||||
| 
						 | 
				
			
			@ -147,7 +165,7 @@
 | 
			
		|||
              ></kubernetes-events-datatable>
 | 
			
		||||
            </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>
 | 
			
		||||
              <div style="padding-right: 25px;" ng-if="ctrl.state.showEditorTab">
 | 
			
		||||
                <kubernetes-yaml-inspector key="application-yaml" data="ctrl.application.Yaml"></kubernetes-yaml-inspector>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,91 @@
 | 
			
		|||
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 KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
 | 
			
		||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
 | 
			
		||||
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 {
 | 
			
		||||
  /* @ngInject */
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +100,7 @@ class KubernetesApplicationController {
 | 
			
		|||
    KubernetesEventService,
 | 
			
		||||
    KubernetesStackService,
 | 
			
		||||
    KubernetesPodService,
 | 
			
		||||
    KubernetesNodeService,
 | 
			
		||||
    KubernetesNamespaceHelper
 | 
			
		||||
  ) {
 | 
			
		||||
    this.$async = $async;
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +114,7 @@ class KubernetesApplicationController {
 | 
			
		|||
    this.KubernetesEventService = KubernetesEventService;
 | 
			
		||||
    this.KubernetesStackService = KubernetesStackService;
 | 
			
		||||
    this.KubernetesPodService = KubernetesPodService;
 | 
			
		||||
    this.KubernetesNodeService = KubernetesNodeService;
 | 
			
		||||
 | 
			
		||||
    this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -103,7 +187,6 @@ class KubernetesApplicationController {
 | 
			
		|||
  /**
 | 
			
		||||
   * ROLLBACK
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  async rollbackApplicationAsync() {
 | 
			
		||||
    try {
 | 
			
		||||
      // await this.KubernetesApplicationService.rollback(this.application, this.formValues.SelectedRevision);
 | 
			
		||||
| 
						 | 
				
			
			@ -196,7 +279,11 @@ class KubernetesApplicationController {
 | 
			
		|||
  async getApplicationAsync() {
 | 
			
		||||
    try {
 | 
			
		||||
      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;
 | 
			
		||||
      if (this.application.Note) {
 | 
			
		||||
        this.state.expandedNote = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -204,6 +291,8 @@ class KubernetesApplicationController {
 | 
			
		|||
      if (this.application.CurrentRevision) {
 | 
			
		||||
        this.formValues.SelectedRevision = _.find(this.application.Revisions, { revision: this.application.CurrentRevision.revision });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.placements = computePlacements(nodes, this.application);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      this.Notifications.error('Failure', err, 'Unable to retrieve application details');
 | 
			
		||||
    } 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