feat(kubernetes): add ingress details (#4013)

* feat(kubernetes): add ingress details

* fix(kubernetes): fix broken ingress generated links + ignore IP retrieval/display info on missing LB ingress ip

* refactor(kubernetes): each ingress rule in apps port mappings has now its own row

* feat(kubernetes): remove protocol column and concat it to container port

* feat(kubernetes): edit display of ingress rules in application details

* feat(kubernetes): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
pull/4029/head
xAt0mZ 4 years ago committed by GitHub
parent b09b1b1691
commit 1b3e2c8f69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -96,15 +96,13 @@
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Protocol')">
Protocol
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Protocol' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Protocol' && $ctrl.state.reverseOrder"></i>
</a>
HTTP route
</th>
</tr>
</thead>
<tbody>
<!-- main rows -->
<!-- dir-paginate-start track by $index -->
<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 }"
@ -112,40 +110,87 @@
ng-click="$ctrl.expandItem(item, !item.Expanded)"
pagination-id="$ctrl.tableKey"
>
<!-- expandable -->
<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>
</td>
<!-- Application -->
<td>
<a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
</td>
<!-- Publishing mode -->
<td>
<span ng-if="item.ServiceType === 'LoadBalancer'">
<!-- LB -->
<span ng-if="item.ServiceType === $ctrl.KubernetesServiceTypes.LOAD_BALANCER">
<span> <i class="fa fa-project-diagram" aria-hidden="true" style="margin-right: 2px;"></i> Load balancer </span>
<span class="text-muted small" style="margin-left: 5px;">
<span ng-if="item.LoadBalancerIPAddress">{{ item.LoadBalancerIPAddress }}</span>
<span ng-if="!item.LoadBalancerIPAddress">pending</span>
</span>
</span>
<span ng-if="item.ServiceType === 'ClusterIP'"> <i class="fa fa-list-alt" aria-hidden="true" style="margin-right: 2px;"></i> Internal </span>
<span ng-if="item.ServiceType === 'NodePort'"> <i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i> Cluster </span>
<!-- Internal -->
<span ng-if="item.ServiceType === $ctrl.KubernetesServiceTypes.CLUSTER_IP">
<i class="fa fa-list-alt" aria-hidden="true" style="margin-right: 2px;"></i> Internal
</span>
<!-- Cluster -->
<span ng-if="item.ServiceType === $ctrl.KubernetesServiceTypes.NODE_PORT"> <i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i> Cluster </span>
</td>
<!-- Exposed port -->
<td>
<span ng-if="!$ctrl.itemCanExpand(item)">
{{ item.Ports[0].Port }}
<a ng-if="item.LoadBalancerIPAddress" ng-href="http://{{ item.LoadBalancerIPAddress }}:{{ item.Ports[0].Port }}" target="_blank" style="margin-left: 5px;">
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a>
</span>
</td>
<!-- Container port -->
<td>
<span ng-if="!$ctrl.itemCanExpand(item)"> {{ item.Ports[0].TargetPort }}/{{ item.Ports[0].Protocol }} </span>
</td>
<!-- HTTP route -->
<td>
<span ng-if="!$ctrl.itemCanExpand(item)">
<span ng-if="!$ctrl.portHasIngressRules(item.Ports[0])">-</span>
<span ng-if="$ctrl.portHasIngressRules(item.Ports[0])">
<span
ng-if="!$ctrl.ruleCanBeDisplayed(item.Ports[0].IngressRules[0])"
class="text-muted"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Ingress controller IP address not available yet"
style="cursor: pointer;"
>pending
</span>
<span ng-if="$ctrl.ruleCanBeDisplayed(item.Ports[0].IngressRules[0])">
<a ng-href="{{ $ctrl.buildIngressRuleURL(item.Ports[0].IngressRules[0]) }}" target="_blank">
{{ $ctrl.buildIngressRuleURL(item.Ports[0].IngressRules[0]) | stripprotocol }}
</a>
</span>
</span>
</span>
</td>
<td ng-if="!$ctrl.itemCanExpand(item)">
{{ item.Ports[0].Port }}
<a ng-if="item.LoadBalancerIPAddress" ng-href="http://{{ item.LoadBalancerIPAddress }}:{{ item.Ports[0].Port }}" target="_blank" style="margin-left: 5px;">
</tr>
<!-- sub rows -->
<tr ng-show="item.Expanded" ng-repeat-start="port in item.Ports" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
<td ng-if="!$ctrl.portHasIngressRules(port)"></td>
<td ng-if="!$ctrl.portHasIngressRules(port)">-</td>
<td ng-if="!$ctrl.portHasIngressRules(port)">-</td>
<td ng-if="!$ctrl.portHasIngressRules(port)">
{{ port.Port }}
<a ng-if="item.LoadBalancerIPAddress" ng-href="http://{{ item.LoadBalancerIPAddress }}:{{ port.Port }}" target="_blank" style="margin-left: 5px;">
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a>
</td>
<td ng-if="!$ctrl.itemCanExpand(item)">{{ item.Ports[0].TargetPort }}</td>
<td ng-if="!$ctrl.itemCanExpand(item)">{{ item.Ports[0].Protocol }}</td>
<td ng-if="$ctrl.itemCanExpand(item)"></td>
<td ng-if="$ctrl.itemCanExpand(item)"></td>
<td ng-if="$ctrl.itemCanExpand(item)"></td>
<td ng-if="!$ctrl.portHasIngressRules(port)">{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td ng-if="!$ctrl.portHasIngressRules(port)">-</td>
</tr>
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="port in item.Ports" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
<tr ng-show="item.Expanded" ng-repeat-end ng-repeat="rule in port.IngressRules" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
<td></td>
<td>-</td>
<td>-</td>
@ -155,12 +200,31 @@
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a>
</td>
<td>{{ port.TargetPort }}</td>
<td>{{ port.Protocol }}</td>
<td>{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td>
<span
ng-if="!$ctrl.ruleCanBeDisplayed(rule)"
class="text-muted"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Ingress controller IP address not available yet"
style="cursor: pointer;"
>pending
</span>
<span ng-if="$ctrl.ruleCanBeDisplayed(rule)">
<a ng-href="{{ $ctrl.buildIngressRuleURL(rule) }}" target="_blank">
{{ $ctrl.buildIngressRuleURL(rule) | stripprotocol }}
</a>
</span>
</td>
</tr>
<tr dir-paginate-end></tr>
<!-- no dataset -->
<tr ng-if="!$ctrl.dataset">
<td colspan="6" class="text-center text-muted">Loading...</td>
</tr>
<!-- no values in filtered dataset -->
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="6" class="text-center text-muted">No application port mapping available.</td>
</tr>

@ -1,6 +1,7 @@
import _ from 'lodash-es';
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatatableController', [
'$scope',
@ -16,6 +17,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatata
});
var ctrl = this;
this.KubernetesServiceTypes = KubernetesServiceTypes;
this.settings = Object.assign(this.settings, {
showSystem: false,
@ -49,7 +51,20 @@ angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatata
};
this.itemCanExpand = function (item) {
return item.Ports.length > 1;
return item.Ports.length > 1 || item.Ports[0].IngressRules.length > 1;
};
this.buildIngressRuleURL = function (rule) {
const hostname = rule.Host ? rule.Host : rule.IP;
return 'http://' + hostname + rule.Path;
};
this.portHasIngressRules = function (port) {
return port.IngressRules.length > 0;
};
this.ruleCanBeDisplayed = function (rule) {
return !rule.Host && !rule.IP ? false : true;
};
this.hasExpandableItems = function () {

@ -1,4 +1,4 @@
import _ from 'lodash-es';
import * as _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import {
@ -25,9 +25,31 @@ import KubernetesStatefulSetConverter from 'Kubernetes/converters/statefulSet';
import KubernetesServiceConverter from 'Kubernetes/converters/service';
import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim';
import PortainerError from 'Portainer/error';
import { KubernetesApplicationPort } from 'Kubernetes/models/application/models';
import { KubernetesIngressHelper } from 'Kubernetes/ingress/helper';
function _apiPortsToPublishedPorts(pList, pRefs) {
const ports = _.map(pList, (item) => {
const res = new KubernetesApplicationPort();
res.Port = item.port;
res.TargetPort = item.targetPort;
res.NodePort = item.nodePort;
res.Protocol = item.protocol;
return res;
});
_.forEach(ports, (port) => {
if (isNaN(port.TargetPort)) {
const targetPort = _.find(pRefs, { name: port.TargetPort });
if (targetPort) {
port.TargetPort = targetPort.containerPort;
}
}
});
return ports;
}
class KubernetesApplicationConverter {
static applicationCommon(res, data, service) {
static applicationCommon(res, data, service, ingressRules) {
res.Id = data.metadata.uid;
res.Name = data.metadata.name;
res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-';
@ -87,16 +109,11 @@ class KubernetesApplicationConverter {
}
}
const ports = _.concat(..._.map(data.spec.template.spec.containers, (container) => container.ports));
res.PublishedPorts = service.spec.ports;
_.forEach(res.PublishedPorts, (publishedPort) => {
if (isNaN(publishedPort.targetPort)) {
const targetPort = _.find(ports, { name: publishedPort.targetPort });
if (targetPort) {
publishedPort.targetPort = targetPort.containerPort;
}
}
});
const portsRefs = _.concat(..._.map(data.spec.template.spec.containers, (container) => container.ports));
const ports = _apiPortsToPublishedPorts(service.spec.ports, portsRefs);
const rules = KubernetesIngressHelper.findSBoundServiceIngressesRules(ingressRules, service);
_.forEach(ports, (port) => (port.IngressRules = _.filter(rules, (rule) => rule.Port === port.Port)));
res.PublishedPorts = ports;
}
res.Volumes = data.spec.template.spec.volumes ? data.spec.template.spec.volumes : [];
@ -193,9 +210,9 @@ class KubernetesApplicationConverter {
);
}
static apiDeploymentToApplication(data, service) {
static apiDeploymentToApplication(data, service, ingressRules) {
const res = new KubernetesApplication();
KubernetesApplicationConverter.applicationCommon(res, data, service);
KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules);
res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT;
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
@ -204,9 +221,9 @@ class KubernetesApplicationConverter {
return res;
}
static apiDaemonSetToApplication(data, service) {
static apiDaemonSetToApplication(data, service, ingressRules) {
const res = new KubernetesApplication();
KubernetesApplicationConverter.applicationCommon(res, data, service);
KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules);
res.ApplicationType = KubernetesApplicationTypes.DAEMONSET;
res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL;
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
@ -215,9 +232,9 @@ class KubernetesApplicationConverter {
return res;
}
static apiStatefulSetToapplication(data, service) {
static apiStatefulSetToapplication(data, service, ingressRules) {
const res = new KubernetesApplication();
KubernetesApplicationConverter.applicationCommon(res, data, service);
KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules);
res.ApplicationType = KubernetesApplicationTypes.STATEFULSET;
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;

@ -1,5 +1,6 @@
import _ from 'lodash-es';
import { KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
angular
.module('portainer.kubernetes')
@ -31,6 +32,19 @@ angular
}
};
})
.filter('kubernetesApplicationPortsTableHeaderText', function () {
'use strict';
return function (serviceType) {
switch (serviceType) {
case KubernetesServiceTypes.LOAD_BALANCER:
return 'Load balancer';
case KubernetesServiceTypes.CLUSTER_IP:
return 'Application';
case KubernetesServiceTypes.NODE_PORT:
return 'Cluster node';
}
};
})
.filter('kubernetesApplicationCPUValue', function () {
'use strict';
return function (value) {

@ -41,9 +41,10 @@ class KubernetesApplicationHelper {
mapping.Ports = _.map(app.PublishedPorts, (item) => {
const port = new KubernetesPortMappingPort();
port.Port = mapping.ServiceType === KubernetesServiceTypes.NODE_PORT ? item.nodePort : item.port;
port.TargetPort = item.targetPort;
port.Protocol = item.protocol;
port.Port = mapping.ServiceType === KubernetesServiceTypes.NODE_PORT ? item.NodePort : item.Port;
port.TargetPort = item.TargetPort;
port.Protocol = item.Protocol;
port.IngressRules = item.IngressRules;
return port;
});
acc.push(mapping);
@ -249,13 +250,13 @@ class KubernetesApplicationHelper {
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) {
const finalRes = _.map(publishedPorts, (port) => {
const res = new KubernetesApplicationPublishedPortFormValue();
res.Protocol = port.protocol;
res.ContainerPort = port.targetPort;
res.Protocol = port.Protocol;
res.ContainerPort = port.TargetPort;
if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) {
res.LoadBalancerPort = port.port;
res.LoadBalancerNodePort = port.nodePort;
res.LoadBalancerPort = port.Port;
res.LoadBalancerNodePort = port.NodePort;
} else if (serviceType === KubernetesServiceTypes.NODE_PORT) {
res.NodePort = port.nodePort;
res.NodePort = port.NodePort;
}
return res;
});

@ -7,7 +7,7 @@ class KubernetesServiceHelper {
}
static findApplicationBoundService(services, rawApp) {
return _.find(services, (item) => _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector));
return _.find(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector));
}
}
export default KubernetesServiceHelper;

@ -0,0 +1,19 @@
import * as _ from 'lodash-es';
import { KubernetesIngressRule } from './models';
export class KubernetesIngressConverter {
static apiToModel(data) {
const rules = _.flatMap(data.spec.rules, (rule) => {
return _.map(rule.http.paths, (path) => {
const ingRule = new KubernetesIngressRule();
ingRule.ServiceName = path.backend.serviceName;
ingRule.Host = rule.host;
ingRule.IP = data.status.loadBalancer.ingress ? data.status.loadBalancer.ingress[0].ip : undefined;
ingRule.Port = path.backend.servicePort;
ingRule.Path = path.path;
return ingRule;
});
});
return rules;
}
}

@ -0,0 +1,7 @@
import * as _ from 'lodash-es';
export class KubernetesIngressHelper {
static findSBoundServiceIngressesRules(ingressRules, service) {
return _.filter(ingressRules, (r) => r.ServiceName === service.metadata.name);
}
}

@ -0,0 +1,16 @@
/**
* KubernetesIngressRule Model
*/
const _KubernetesIngressRule = Object.freeze({
ServiceName: '',
Host: '',
IP: '',
Port: '',
Path: '',
});
export class KubernetesIngressRule {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesIngressRule)));
}
}

@ -0,0 +1,50 @@
import { rawResponse } from 'Kubernetes/rest/response/transform';
angular.module('portainer.kubernetes').factory('KubernetesIngresses', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function KubernetesIngressesFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return function (namespace) {
const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/networking.k8s.io/v1beta1' + (namespace ? '/namespaces/:namespace' : '') + '/ingresses/:id/:action';
return $resource(
url,
{
endpointId: EndpointProvider.endpointID,
namespace: namespace,
},
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {
method: 'GET',
headers: {
Accept: 'application/yaml',
},
transformResponse: rawResponse,
ignoreLoadingBar: true,
},
create: { method: 'POST' },
update: { method: 'PUT' },
patch: {
method: 'PATCH',
headers: {
'Content-Type': 'application/json-patch+json',
},
},
rollback: {
method: 'PATCH',
headers: {
'Content-Type': 'application/json-patch+json',
},
},
delete: { method: 'DELETE' },
}
);
};
},
]);

@ -0,0 +1,54 @@
import * as _ from 'lodash-es';
import angular from 'angular';
import PortainerError from 'Portainer/error';
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
import { KubernetesIngressConverter } from './converter';
class KubernetesIngressService {
/* @ngInject */
constructor($async, KubernetesIngresses) {
this.$async = $async;
this.KubernetesIngresses = KubernetesIngresses;
this.getAsync = this.getAsync.bind(this);
this.getAllAsync = this.getAllAsync.bind(this);
}
/**
* GET
*/
async getAsync(namespace, name) {
try {
const params = new KubernetesCommonParams();
params.id = name;
const [raw, yaml] = await Promise.all([this.KubernetesIngresses(namespace).get(params).$promise, this.KubernetesIngresses(namespace).getYaml(params).$promise]);
const res = {
Raw: KubernetesIngressConverter.apiToModel(raw),
Yaml: yaml.data,
};
return res;
} catch (err) {
throw new PortainerError('Unable to retrieve Ingress', err);
}
}
async getAllAsync(namespace) {
try {
const data = await this.KubernetesIngresses(namespace).get().$promise;
const res = _.reduce(data.items, (arr, item) => _.concat(arr, KubernetesIngressConverter.apiToModel(item)), []);
return res;
} catch (err) {
throw new PortainerError('Unable to retrieve Ingresses', err);
}
}
get(namespace, name) {
if (name) {
return this.$async(this.getAsync, namespace, name);
}
return this.$async(this.getAllAsync, namespace);
}
}
export default KubernetesIngressService;
angular.module('portainer.kubernetes').service('KubernetesIngressService', KubernetesIngressService);

@ -112,3 +112,20 @@ export class KubernetesApplicationConfigurationVolume {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationConfigurationVolume)));
}
}
/**
* KubernetesApplicationPort Model
*/
const _KubernetesApplicationPort = Object.freeze({
IngressRules: [], // KubernetesIngressRule[]
NodePort: 0,
TargetPort: 0,
Port: 0,
Protocol: '',
});
export class KubernetesApplicationPort {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPort)));
}
}

@ -5,6 +5,7 @@ const _KubernetesPortMappingPort = Object.freeze({
Port: 0,
TargetPort: 0,
Protocol: '',
IngressRules: [], // KubernetesIngressRule[]
});
export class KubernetesPortMappingPort {

@ -3,6 +3,7 @@ export const KubernetesServiceHeadlessClusterIP = 'None';
export const KubernetesServiceTypes = Object.freeze({
LOAD_BALANCER: 'LoadBalancer',
NODE_PORT: 'NodePort',
CLUSTER_IP: 'ClusterIP',
});
/**

@ -27,7 +27,8 @@ class KubernetesApplicationService {
KubernetesNamespaceService,
KubernetesPodService,
KubernetesHistoryService,
KubernetesHorizontalPodAutoScalerService
KubernetesHorizontalPodAutoScalerService,
KubernetesIngressService
) {
this.$async = $async;
this.Authentication = Authentication;
@ -41,6 +42,7 @@ class KubernetesApplicationService {
this.KubernetesPodService = KubernetesPodService;
this.KubernetesHistoryService = KubernetesHistoryService;
this.KubernetesHorizontalPodAutoScalerService = KubernetesHorizontalPodAutoScalerService;
this.KubernetesIngressService = KubernetesIngressService;
this.getAsync = this.getAsync.bind(this);
this.getAllAsync = this.getAllAsync.bind(this);
@ -73,25 +75,26 @@ class KubernetesApplicationService {
*/
async getAsync(namespace, name) {
try {
const [deployment, daemonSet, statefulSet, pods, autoScalers] = await Promise.allSettled([
const [deployment, daemonSet, statefulSet, pods, autoScalers, ingresses] = await Promise.allSettled([
this.KubernetesDeploymentService.get(namespace, name),
this.KubernetesDaemonSetService.get(namespace, name),
this.KubernetesStatefulSetService.get(namespace, name),
this.KubernetesPodService.get(namespace),
this.KubernetesHorizontalPodAutoScalerService.get(namespace),
this.KubernetesIngressService.get(namespace),
]);
let rootItem;
let converterFunction;
let converterFunc;
if (deployment.status === 'fulfilled') {
rootItem = deployment;
converterFunction = KubernetesApplicationConverter.apiDeploymentToApplication;
converterFunc = KubernetesApplicationConverter.apiDeploymentToApplication;
} else if (daemonSet.status === 'fulfilled') {
rootItem = daemonSet;
converterFunction = KubernetesApplicationConverter.apiDaemonSetToApplication;
converterFunc = KubernetesApplicationConverter.apiDaemonSetToApplication;
} else if (statefulSet.status === 'fulfilled') {
rootItem = statefulSet;
converterFunction = KubernetesApplicationConverter.apiStatefulSetToapplication;
converterFunc = KubernetesApplicationConverter.apiStatefulSetToapplication;
} else {
throw new PortainerError('Unable to determine which association to use');
}
@ -100,7 +103,7 @@ class KubernetesApplicationService {
const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw);
const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {};
const application = converterFunction(rootItem.value.Raw, service.Raw);
const application = converterFunc(rootItem.value.Raw, service.Raw, ingresses.value);
application.Yaml = rootItem.value.Yaml;
application.Raw = rootItem.value.Raw;
application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods.value, application.Raw);
@ -117,6 +120,8 @@ class KubernetesApplicationService {
if (scaler && scaler.Yaml) {
application.Yaml += '---\n' + scaler.Yaml;
}
// TODO: refactor
// append ingress yaml ?
return application;
} catch (err) {
throw err;
@ -126,33 +131,35 @@ class KubernetesApplicationService {
async getAllAsync(namespace) {
try {
const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name');
const convertToApplication = (item, converterFunc, services, pods, ingresses) => {
const service = KubernetesServiceHelper.findApplicationBoundService(services, item);
const application = converterFunc(item, service, ingresses);
application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item);
return application;
};
const res = await Promise.all(
_.map(namespaces, async (ns) => {
const [deployments, daemonSets, statefulSets, services, pods] = await Promise.all([
const [deployments, daemonSets, statefulSets, services, pods, ingresses] = await Promise.all([
this.KubernetesDeploymentService.get(ns),
this.KubernetesDaemonSetService.get(ns),
this.KubernetesStatefulSetService.get(ns),
this.KubernetesServiceService.get(ns),
this.KubernetesPodService.get(ns),
this.KubernetesIngressService.get(ns),
]);
const deploymentApplications = _.map(deployments, (item) => {
const service = KubernetesServiceHelper.findApplicationBoundService(services, item);
const application = KubernetesApplicationConverter.apiDeploymentToApplication(item, service);
application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item);
return application;
});
const daemonSetApplications = _.map(daemonSets, (item) => {
const service = KubernetesServiceHelper.findApplicationBoundService(services, item);
const application = KubernetesApplicationConverter.apiDaemonSetToApplication(item, service);
application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item);
return application;
});
const statefulSetApplications = _.map(statefulSets, (item) => {
const service = KubernetesServiceHelper.findApplicationBoundService(services, item);
const application = KubernetesApplicationConverter.apiStatefulSetToapplication(item, service);
application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item);
return application;
});
const deploymentApplications = _.map(deployments, (item) =>
convertToApplication(item, KubernetesApplicationConverter.apiDeploymentToApplication, services, pods, ingresses)
);
const daemonSetApplications = _.map(daemonSets, (item) =>
convertToApplication(item, KubernetesApplicationConverter.apiDaemonSetToApplication, services, pods, ingresses)
);
const statefulSetApplications = _.map(statefulSets, (item) =>
convertToApplication(item, KubernetesApplicationConverter.apiStatefulSetToapplication, services, pods, ingresses)
);
return _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications);
})
);

@ -87,9 +87,7 @@ class KubernetesApplicationsController {
item.Expanded = false;
item.Highlighted = false;
if (item.Name === application.Name) {
if (item.Ports.length > 1) {
item.Expanded = true;
}
item.Expanded = true;
item.Highlighted = true;
}
});

@ -180,7 +180,8 @@
</div>
<div ng-if="ctrl.application.PublishedPorts.length > 0">
<div ng-if="ctrl.application.ServiceType === 'LoadBalancer'">
<!-- LB notice -->
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.LOAD_BALANCER">
<div class="small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This application is exposed through an external load balancer. Use the links below to access the different ports exposed.
@ -210,55 +211,19 @@
</p>
</span>
</div>
<div style="margin-top: 15px; width: 33%;">
<table class="table">
<tbody>
<tr class="text-muted">
<td style="width: 50%;">Container port</td>
<td style="width: 50%;">Load balancer port</td>
</tr>
<tr ng-repeat="port in ctrl.application.PublishedPorts track by $index">
<td>{{ port.targetPort }}</td>
<td>
{{ port.port }}
<a
ng-if="ctrl.application.LoadBalancerIPAddress"
ng-href="http://{{ ctrl.application.LoadBalancerIPAddress }}:{{ port.port }}"
target="_blank"
style="margin-left: 5px;"
>
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div ng-if="ctrl.application.ServiceType === 'NodePort'">
<!-- cluster notice -->
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT">
<div class="small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This application is exposed globally on all nodes of your cluster. It can be reached using the IP address of any node in your cluster using the port configuration
below.
</div>
<div style="margin-top: 15px; width: 33%;">
<table class="table">
<tbody>
<tr class="text-muted">
<td style="width: 50%;">Container port</td>
<td style="width: 50%;">Cluster node port</td>
</tr>
<tr ng-repeat="port in ctrl.application.PublishedPorts track by $index">
<td>{{ port.targetPort }}</td>
<td>{{ port.nodePort }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div ng-if="ctrl.application.ServiceType === 'ClusterIP'">
<!-- internal notice -->
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.CLUSTER_IP">
<div class="small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This application is only available for internal usage inside the cluster via the application name <code>{{ ctrl.application.ServiceName }}</code>
@ -270,22 +235,75 @@
<div class="small text-muted" style="margin-top: 2px;">
<p>Refer to the below port configuration to access the application.</p>
</div>
<div style="margin-top: 15px; width: 50%;">
<table class="table">
<tbody>
<tr class="text-muted">
<td style="width: 33%;">Container port</td>
<td style="width: 33%;">Application port</td>
<td style="width: 33%;">Protocol</td>
</tr>
<tr ng-repeat="port in ctrl.application.PublishedPorts track by $index">
<td>{{ port.targetPort }}</td>
<td>{{ port.port }}</td>
<td>{{ port.protocol }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- table -->
<div style="margin-top: 15px;">
<table class="table">
<tbody>
<tr class="text-muted">
<td style="width: 25%;">Container port</td>
<td style="width: 25%;">{{ ctrl.application.ServiceType | kubernetesApplicationPortsTableHeaderText }} port</td>
<td style="width: 50%;">HTTP route</td>
</tr>
<tr ng-repeat-start="port in ctrl.application.PublishedPorts">
<td ng-if="!ctrl.portHasIngressRules(port)">{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td ng-if="!ctrl.portHasIngressRules(port)">
<span ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT">
{{ port.NodePort }}
</span>
<span ng-if="ctrl.application.ServiceType !== ctrl.KubernetesServiceTypes.NODE_PORT">
{{ port.Port }}
</span>
<a
ng-if="ctrl.application.LoadBalancerIPAddress"
ng-href="http://{{ ctrl.application.LoadBalancerIPAddress }}:{{ port.Port }}"
target="_blank"
style="margin-left: 5px;"
>
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a>
</td>
<td ng-if="!ctrl.portHasIngressRules(port)">-</td>
</tr>
<tr ng-repeat-end ng-repeat="rule in port.IngressRules">
<td>{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td>
<span ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT">
{{ port.NodePort }}
</span>
<span ng-if="ctrl.application.ServiceType !== ctrl.KubernetesServiceTypes.NODE_PORT">
{{ port.Port }}
</span>
<a
ng-if="ctrl.application.LoadBalancerIPAddress"
ng-href="http://{{ ctrl.application.LoadBalancerIPAddress }}:{{ port.Port }}"
target="_blank"
style="margin-left: 5px;"
>
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a>
</td>
<td>
<span
ng-if="!ctrl.ruleCanBeDisplayed(rule)"
class="text-muted"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Ingress controller IP address not available yet"
style="cursor: pointer;"
>pending
</span>
<span ng-if="ctrl.ruleCanBeDisplayed(rule)">
<a ng-href="{{ ctrl.buildIngressRuleURL(rule) }}" target="_blank">
{{ ctrl.buildIngressRuleURL(rule) | stripprotocol }}
</a>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- !ACCESSING APPLICATION -->
@ -306,10 +324,8 @@
<td style="width: 33%;">Maximum instances</td>
<td style="width: 33%;">
Target CPU usage
<portainer-tooltip
position="bottom"
message="The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances."
></portainer-tooltip>
<portainer-tooltip position="bottom" message="The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances.">
</portainer-tooltip>
</td>
</tr>
<tr>

@ -3,6 +3,7 @@ import _ from 'lodash-es';
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';
class KubernetesApplicationController {
/* @ngInject */
@ -34,6 +35,7 @@ class KubernetesApplicationController {
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
this.KubernetesServiceTypes = KubernetesServiceTypes;
this.onInit = this.onInit.bind(this);
this.getApplication = this.getApplication.bind(this);
@ -85,6 +87,19 @@ class KubernetesApplicationController {
return this.state.eventWarningCount;
}
buildIngressRuleURL(rule) {
const hostname = rule.Host ? rule.Host : rule.IP;
return 'http://' + hostname + rule.Path;
}
portHasIngressRules(port) {
return port.IngressRules.length > 0;
}
ruleCanBeDisplayed(rule) {
return !rule.Host && !rule.IP ? false : true;
}
/**
* ROLLBACK
*/

@ -3,6 +3,7 @@
"target": "es2017",
"allowSyntheticDefaultImports": false,
"baseUrl": "app",
"module": "commonjs",
"paths": {
"Agent/*": ["agent/*"],
"Azure/*": ["azure/*"],

Loading…
Cancel
Save