1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-23 07:19:41 +02:00

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>
This commit is contained in:
xAt0mZ 2020-07-14 22:45:19 +02:00 committed by GitHub
parent b09b1b1691
commit 1b3e2c8f69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 450 additions and 137 deletions

View file

@ -96,15 +96,13 @@
</a> </a>
</th> </th>
<th> <th>
<a ng-click="$ctrl.changeOrderBy('Protocol')"> HTTP route
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>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- main rows -->
<!-- dir-paginate-start track by $index -->
<tr <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))" 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-class="{ active: item.Checked }"
@ -112,40 +110,87 @@
ng-click="$ctrl.expandItem(item, !item.Expanded)" ng-click="$ctrl.expandItem(item, !item.Expanded)"
pagination-id="$ctrl.tableKey" pagination-id="$ctrl.tableKey"
> >
<!-- expandable -->
<td> <td>
<a ng-if="$ctrl.itemCanExpand(item)"> <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> <i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i>
</a> </a>
</td> </td>
<!-- Application -->
<td> <td>
<a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a> <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-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> <span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
</td> </td>
<!-- Publishing mode -->
<td> <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> <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 class="text-muted small" style="margin-left: 5px;">
<span ng-if="item.LoadBalancerIPAddress">{{ item.LoadBalancerIPAddress }}</span> <span ng-if="item.LoadBalancerIPAddress">{{ item.LoadBalancerIPAddress }}</span>
<span ng-if="!item.LoadBalancerIPAddress">pending</span> <span ng-if="!item.LoadBalancerIPAddress">pending</span>
</span> </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> <!-- Internal -->
<span ng-if="item.ServiceType === 'NodePort'"> <i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i> Cluster </span> <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> </td>
<td ng-if="!$ctrl.itemCanExpand(item)"> <!-- Exposed port -->
<td>
<span ng-if="!$ctrl.itemCanExpand(item)">
{{ item.Ports[0].Port }} {{ item.Ports[0].Port }}
<a ng-if="item.LoadBalancerIPAddress" ng-href="http://{{ item.LoadBalancerIPAddress }}:{{ item.Ports[0].Port }}" target="_blank" style="margin-left: 5px;"> <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 <i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a> </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>
<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>
</tr> </tr>
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="port in item.Ports" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }"> <!-- 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.portHasIngressRules(port)">{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td ng-if="!$ctrl.portHasIngressRules(port)">-</td>
</tr>
<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> <td>-</td>
<td>-</td> <td>-</td>
@ -155,12 +200,31 @@
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access <i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a> </a>
</td> </td>
<td>{{ port.TargetPort }}</td> <td>{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td>{{ 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>
<tr dir-paginate-end></tr>
<!-- no dataset -->
<tr ng-if="!$ctrl.dataset"> <tr ng-if="!$ctrl.dataset">
<td colspan="6" class="text-center text-muted">Loading...</td> <td colspan="6" class="text-center text-muted">Loading...</td>
</tr> </tr>
<!-- no values in filtered dataset -->
<tr ng-if="$ctrl.state.filteredDataSet.length === 0"> <tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="6" class="text-center text-muted">No application port mapping available.</td> <td colspan="6" class="text-center text-muted">No application port mapping available.</td>
</tr> </tr>

View file

@ -1,6 +1,7 @@
import _ from 'lodash-es'; import _ from 'lodash-es';
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatatableController', [ angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatatableController', [
'$scope', '$scope',
@ -16,6 +17,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatata
}); });
var ctrl = this; var ctrl = this;
this.KubernetesServiceTypes = KubernetesServiceTypes;
this.settings = Object.assign(this.settings, { this.settings = Object.assign(this.settings, {
showSystem: false, showSystem: false,
@ -49,7 +51,20 @@ angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatata
}; };
this.itemCanExpand = function (item) { 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 () { this.hasExpandableItems = function () {

View file

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

View file

@ -1,5 +1,6 @@
import _ from 'lodash-es'; 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';
angular angular
.module('portainer.kubernetes') .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 () { .filter('kubernetesApplicationCPUValue', function () {
'use strict'; 'use strict';
return function (value) { return function (value) {

View file

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

View file

@ -7,7 +7,7 @@ class KubernetesServiceHelper {
} }
static findApplicationBoundService(services, rawApp) { 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; export default KubernetesServiceHelper;

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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)));
}
}

View file

@ -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' },
}
);
};
},
]);

View file

@ -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);

View file

@ -112,3 +112,20 @@ export class KubernetesApplicationConfigurationVolume {
Object.assign(this, JSON.parse(JSON.stringify(_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)));
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -180,7 +180,8 @@
</div> </div>
<div ng-if="ctrl.application.PublishedPorts.length > 0"> <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"> <div class="small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i> <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. This application is exposed through an external load balancer. Use the links below to access the different ports exposed.
@ -210,55 +211,19 @@
</p> </p>
</span> </span>
</div> </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>
<div ng-if="ctrl.application.ServiceType === 'NodePort'"> <!-- cluster notice -->
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT">
<div class="small text-muted"> <div class="small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i> <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 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. below.
</div> </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>
<div ng-if="ctrl.application.ServiceType === 'ClusterIP'"> <!-- internal notice -->
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.CLUSTER_IP">
<div class="small text-muted"> <div class="small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i> <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> This application is only available for internal usage inside the cluster via the application name <code>{{ ctrl.application.ServiceName }}</code>
@ -270,24 +235,77 @@
<div class="small text-muted" style="margin-top: 2px;"> <div class="small text-muted" style="margin-top: 2px;">
<p>Refer to the below port configuration to access the application.</p> <p>Refer to the below port configuration to access the application.</p>
</div> </div>
<div style="margin-top: 15px; width: 50%;"> </div>
<!-- table -->
<div style="margin-top: 15px;">
<table class="table"> <table class="table">
<tbody> <tbody>
<tr class="text-muted"> <tr class="text-muted">
<td style="width: 33%;">Container port</td> <td style="width: 25%;">Container port</td>
<td style="width: 33%;">Application port</td> <td style="width: 25%;">{{ ctrl.application.ServiceType | kubernetesApplicationPortsTableHeaderText }} port</td>
<td style="width: 33%;">Protocol</td> <td style="width: 50%;">HTTP route</td>
</tr> </tr>
<tr ng-repeat="port in ctrl.application.PublishedPorts track by $index"> <tr ng-repeat-start="port in ctrl.application.PublishedPorts">
<td>{{ port.targetPort }}</td> <td ng-if="!ctrl.portHasIngressRules(port)">{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td>{{ port.port }}</td> <td ng-if="!ctrl.portHasIngressRules(port)">
<td>{{ port.protocol }}</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 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> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div>
<!-- !ACCESSING APPLICATION --> <!-- !ACCESSING APPLICATION -->
<!-- AUTO SCALING --> <!-- AUTO SCALING -->
<div class="text-muted" style="margin-bottom: 15px;"> <i class="fa fa-expand-arrows-alt" aria-hidden="true" style="margin-right: 2px;"></i> Auto-scaling</div> <div class="text-muted" style="margin-bottom: 15px;"> <i class="fa fa-expand-arrows-alt" aria-hidden="true" style="margin-right: 2px;"></i> Auto-scaling</div>
@ -306,10 +324,8 @@
<td style="width: 33%;">Maximum instances</td> <td style="width: 33%;">Maximum instances</td>
<td style="width: 33%;"> <td style="width: 33%;">
Target CPU usage Target CPU usage
<portainer-tooltip <portainer-tooltip position="bottom" message="The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances.">
position="bottom" </portainer-tooltip>
message="The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances."
></portainer-tooltip>
</td> </td>
</tr> </tr>
<tr> <tr>

View file

@ -3,6 +3,7 @@ import _ from 'lodash-es';
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';
class KubernetesApplicationController { class KubernetesApplicationController {
/* @ngInject */ /* @ngInject */
@ -34,6 +35,7 @@ class KubernetesApplicationController {
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies; this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
this.KubernetesServiceTypes = KubernetesServiceTypes;
this.onInit = this.onInit.bind(this); this.onInit = this.onInit.bind(this);
this.getApplication = this.getApplication.bind(this); this.getApplication = this.getApplication.bind(this);
@ -85,6 +87,19 @@ class KubernetesApplicationController {
return this.state.eventWarningCount; 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 * ROLLBACK
*/ */

View file

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