mirror of https://github.com/portainer/portainer
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
parent
b09b1b1691
commit
1b3e2c8f69
|
@ -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>
|
||||
<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;">
|
||||
<!-- 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>
|
||||
</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…
Reference in New Issue