mirror of https://github.com/portainer/portainer
feat(k8s): Allow mix services for k8s app EE-1791 (#6198)
allow a mix of services for k8s in uipull/4499/merge
parent
edf048570b
commit
c47e840b37
|
@ -166,7 +166,7 @@
|
|||
Status
|
||||
</th>
|
||||
<th>
|
||||
Publishing mode
|
||||
Published
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('CreationDate')">
|
||||
|
@ -233,15 +233,9 @@
|
|||
{{ item.Pods[0].Status }}
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="item.PublishedPorts.length">
|
||||
<span>
|
||||
<a ng-click="$ctrl.onPublishingModeClick(item); $event.stopPropagation()">
|
||||
<i class="fa {{ item.ServiceType | kubernetesApplicationServiceTypeIcon }}" aria-hidden="true" style="margin-right: 2px;"> </i>
|
||||
{{ item.ServiceType | kubernetesApplicationServiceTypeText }}
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
{{ item.Services.length === 0 ? 'No' : 'Yes' }}
|
||||
</span>
|
||||
<span ng-if="item.PublishedPorts.length === 0">-</span>
|
||||
</td>
|
||||
<td>{{ item.CreationDate | getisodate }} {{ item.ApplicationOwner ? 'by ' + item.ApplicationOwner : '' }}</td>
|
||||
</tr>
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import _ from 'lodash-es';
|
||||
import { KubernetesServicePort, KubernetesIngressServiceRoute } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models/constants';
|
||||
|
||||
export default class KubeServicesItemViewController {
|
||||
/* @ngInject */
|
||||
constructor(EndpointProvider, Authentication) {
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.Authentication = Authentication;
|
||||
}
|
||||
|
||||
addPort() {
|
||||
const p = new KubernetesServicePort();
|
||||
p.nodePort = '';
|
||||
p.port = '';
|
||||
p.targetPort = '';
|
||||
p.protocol = 'TCP';
|
||||
|
||||
if (this.ingressType) {
|
||||
const r = new KubernetesIngressServiceRoute();
|
||||
r.ServiceName = this.serviceName;
|
||||
p.ingress = r;
|
||||
p.Ingress = true;
|
||||
}
|
||||
this.servicePorts.push(p);
|
||||
}
|
||||
|
||||
removePort(index) {
|
||||
this.servicePorts.splice(index, 1);
|
||||
}
|
||||
|
||||
servicePort(index) {
|
||||
const targetPort = this.servicePorts[index].targetPort;
|
||||
this.servicePorts[index].port = targetPort;
|
||||
}
|
||||
|
||||
isAdmin() {
|
||||
return this.Authentication.isAdmin();
|
||||
}
|
||||
|
||||
onChangeContainerPort() {
|
||||
const state = this.state.duplicates.targetPort;
|
||||
const source = _.map(this.servicePorts, (sp) => sp.targetPort);
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
state.refs = duplicates;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
onChangeServicePort() {
|
||||
const state = this.state.duplicates.servicePort;
|
||||
const source = _.map(this.servicePorts, (sp) => sp.port);
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
state.refs = duplicates;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
onChangeNodePort() {
|
||||
const state = this.state.duplicates.nodePort;
|
||||
const source = _.map(this.servicePorts, (sp) => sp.nodePort);
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
state.refs = duplicates;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
if (this.servicePorts.length === 0) {
|
||||
this.addPort();
|
||||
}
|
||||
|
||||
this.KubernetesApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
|
||||
|
||||
this.state = {
|
||||
duplicates: {
|
||||
targetPort: new KubernetesFormValidationReferences(),
|
||||
servicePort: new KubernetesFormValidationReferences(),
|
||||
nodePort: new KubernetesFormValidationReferences(),
|
||||
},
|
||||
endpointId: this.EndpointProvider.endpointID(),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
<form name="serviceForm">
|
||||
<div ng-if="$ctrl.isAdmin()" class="small text-warning" ng-show="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled">
|
||||
<p style="margin-top: 10px">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> No Load balancer is available in this cluster, click
|
||||
<a ui-sref="portainer.k8sendpoint.kubernetesConfig({id: $ctrl.state.endpointId})">here</a> to configure load balancer.
|
||||
</p>
|
||||
</div>
|
||||
<div ng-if="!$ctrl.isAdmin()" class="small text-warning" ng-show="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled">
|
||||
<p style="margin-top: 10px"> <i class="fa fa-exclamation-circle" aria-hidden="true"></i> No Load balancer is available in this cluster, contract your administrator. </p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ng-if="
|
||||
($ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && $ctrl.loadbalancerEnabled) ||
|
||||
$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP ||
|
||||
$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT
|
||||
"
|
||||
>
|
||||
<div ng-show="!$ctrl.multiItemDisable" style="margin-top: 5px; margin-bottom: 5px">
|
||||
<label class="control-label text-left">Published ports</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px" ng-click="$ctrl.addPort()" data-cy="k8sAppCreate-addNewPortButton">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> publish a new port
|
||||
</span>
|
||||
</div>
|
||||
<div ng-repeat="servicePort in $ctrl.servicePorts" style="margin-top: 10px">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon">container port</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="container_port_{{ $index }}"
|
||||
ng-model="servicePort.targetPort"
|
||||
placeholder="80"
|
||||
ng-min="1"
|
||||
ng-max="65535"
|
||||
ng-change="$ctrl.servicePort($index)"
|
||||
required
|
||||
ng-disabled="$ctrl.originalIngresses.length === 0 || ($ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled)"
|
||||
ng-change="$ctrl.onChangeContainerPort()"
|
||||
data-cy="k8sAppCreate-containerPort_{{ $index }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon">service port</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="service_port_{{ $index }}"
|
||||
ng-model="servicePort.port"
|
||||
placeholder="80"
|
||||
ng-min="1"
|
||||
ng-max="65535"
|
||||
required
|
||||
ng-disabled="$ctrl.originalIngresses.length === 0 || ($ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled)"
|
||||
ng-change="$ctrl.onChangeServicePort()"
|
||||
data-cy="k8sAppCreate-servicePort_{{ $index }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT" class="input-group input-group-sm">
|
||||
<span class="input-group-addon">nodeport</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="node_port_{{ $index }}"
|
||||
ng-model="servicePort.nodePort"
|
||||
placeholder="30080"
|
||||
ng-min="30000"
|
||||
ng-max="32767"
|
||||
ng-change="$ctrl.onChangeNodePort()"
|
||||
data-cy="k8sAppCreate-nodeportPort_{{ $index }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER" class="input-group input-group-sm">
|
||||
<span class="input-group-addon">loadbalancer port</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="loadbalancer_port_{{ $index }}"
|
||||
ng-model="servicePort.port"
|
||||
placeholder="80"
|
||||
ng-min="1"
|
||||
ng-max="65535"
|
||||
required
|
||||
ng-disabled="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled"
|
||||
data-cy="k8sAppCreate-loadbalancerPort_{{ $index }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP && $ctrl.ingressType" class="input-group input-group-sm">
|
||||
<span class="input-group-addon">ingress</span>
|
||||
<select
|
||||
ng-init="servicePort.ingress.IngressName = $ctrl.originalIngresses[0].Name"
|
||||
class="form-control"
|
||||
name="ingress_port_{{ $index }}"
|
||||
ng-model="servicePort.ingress.IngressName"
|
||||
required
|
||||
ng-disabled="$ctrl.originalIngresses.length === 0"
|
||||
ng-options="ingress.Name as ingress.Name for ingress in $ctrl.originalIngresses"
|
||||
data-cy="k8sAppCreate-ingressPort_{{ $index }}"
|
||||
>
|
||||
<option selected disabled hidden value="">Select an ingress</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP && $ctrl.ingressType" class="input-group input-group-sm">
|
||||
<span class="input-group-addon">hostname</span>
|
||||
<select
|
||||
ng-init="servicePort.ingress.Host = $ctrl.originalIngresses[0].Hosts"
|
||||
class="form-control"
|
||||
name="hostname_port_{{ $index }}"
|
||||
ng-model="servicePort.ingress.Host"
|
||||
required
|
||||
ng-disabled="$ctrl.originalIngresses.length === 0"
|
||||
ng-options="ingress.Hosts as ingress.Hosts for ingress in $ctrl.originalIngresses"
|
||||
data-cy="k8sAppCreate-hostnamePort_{{ $index }}"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a hostname</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP && $ctrl.ingressType" class="input-group input-group-sm">
|
||||
<span class="input-group-addon">route</span>
|
||||
<input
|
||||
class="form-control"
|
||||
name="ingress_route_{{ $index }}"
|
||||
ng-model="servicePort.ingress.Path"
|
||||
placeholder="route"
|
||||
required
|
||||
ng-disabled="$ctrl.originalIngresses.length === 0"
|
||||
ng-pattern="/^(\/?[a-zA-Z0-9]+([a-zA-Z0-9-/_]*[a-zA-Z0-9])?|[a-zA-Z0-9]+)|(\/){1}$/"
|
||||
data-cy="k8sAppCreate-route_{{ $index }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-2 input-group-sm">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label
|
||||
class="btn btn-primary"
|
||||
ng-model="servicePort.protocol"
|
||||
uib-btn-radio="'TCP'"
|
||||
ng-change="ctrl.onChangePortProtocol($index)"
|
||||
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'TCP')"
|
||||
data-cy="k8sAppCreate-TCPButton_{{ $index }}"
|
||||
>TCP</label
|
||||
>
|
||||
<label
|
||||
class="btn btn-primary"
|
||||
ng-model="servicePort.protocol"
|
||||
uib-btn-radio="'UDP'"
|
||||
ng-change="ctrl.onChangePortProtocol($index)"
|
||||
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'UDP')"
|
||||
data-cy="k8sAppCreate-UDPButton_{{ $index }}"
|
||||
>UDP</label
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
ng-disabled="$ctrl.servicePorts.length === 1"
|
||||
ng-show="!$ctrl.multiItemDisable"
|
||||
class="btn btn-sm btn-danger"
|
||||
type="button"
|
||||
ng-click="$ctrl.removePort($index)"
|
||||
data-cy="k8sAppCreate-rmPortButton_{{ $index }}"
|
||||
>
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 input-group input-group-sm">
|
||||
<div class="col-sm-2">
|
||||
<div class="small text-warning" style="margin-top: 5px">
|
||||
<p ng-if="$ctrl.state.duplicates.targetPort.refs[$index] !== undefined">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This container port is already used.
|
||||
</p>
|
||||
</div>
|
||||
<div class="small text-warning" ng-messages="serviceForm['container_port_'+$index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number is required.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="small text-warning" style="margin-top: 5px">
|
||||
<p ng-if="$ctrl.state.duplicates.servicePort.refs[$index] !== undefined">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This service port is already used.
|
||||
</p>
|
||||
</div>
|
||||
<div class="small text-warning" style="margin-top: 5px">
|
||||
<div ng-messages="serviceForm['service_port_'+$index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Service port number is required.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="small text-warning" style="margin-top: 5px">
|
||||
<div ng-messages="serviceForm['node_port_'+$index].$error">
|
||||
<p ng-message="min"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767 or blank for system allocated.</p
|
||||
>
|
||||
<p ng-message="max"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767 or blank for system allocated.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="small text-warning" style="margin-top: 5px">
|
||||
<div ng-messages="serviceForm['ingress_port_'+$index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Ingress selection is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="small text-warning" style="margin-top: 5px">
|
||||
<div ng-messages="serviceForm['hostname_port_'+$index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Host is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-8" ng-show="">
|
||||
<div class="small text-warning" style="margin-top: 5px">
|
||||
<div ng-messages="serviceForm['ingress_route_'+$index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Route is required.</p>
|
||||
<p ng-message="pattern"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of alphanumeric characters or the special characters: '-', '_' or '/'. It
|
||||
must start and end with an alphanumeric character (e.g. 'my-route', or 'route-123').</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,19 @@
|
|||
import angular from 'angular';
|
||||
import controller from './kube-services-item.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubeServicesItemView', {
|
||||
templateUrl: './kube-services-item.html',
|
||||
controller,
|
||||
bindings: {
|
||||
serviceType: '<',
|
||||
servicePorts: '=',
|
||||
serviceRoutes: '=',
|
||||
ingressType: '<',
|
||||
originalIngresses: '<',
|
||||
isEdit: '<',
|
||||
serviceName: '<',
|
||||
multiItemDisable: '<',
|
||||
serviceIndex: '<',
|
||||
loadbalancerEnabled: '<',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
import { KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models/constants';
|
||||
|
||||
export default class KubeServicesViewController {
|
||||
/* @ngInject */
|
||||
constructor($async, EndpointProvider, Authentication) {
|
||||
this.$async = $async;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.Authentication = Authentication;
|
||||
}
|
||||
|
||||
addEntry(service) {
|
||||
const p = new KubernetesService();
|
||||
if (service === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
p.Type = KubernetesApplicationPublishingTypes.CLUSTER_IP;
|
||||
p.Ingress = true;
|
||||
} else {
|
||||
p.Type = service;
|
||||
}
|
||||
|
||||
p.Selector = this.formValues.Selector;
|
||||
|
||||
p.Name = this.getUniqName();
|
||||
this.state.nameIndex += 1;
|
||||
this.formValues.Services.push(p);
|
||||
}
|
||||
|
||||
getUniqName() {
|
||||
let name = this.formValues.Name + '-' + this.state.nameIndex;
|
||||
const services = this.formValues.Services;
|
||||
services.forEach((service) => {
|
||||
if (service.Name === name) {
|
||||
this.state.nameIndex += 1;
|
||||
name = this.formValues.Name + '-' + this.state.nameIndex;
|
||||
}
|
||||
});
|
||||
const UniqName = this.formValues.Name + '-' + this.state.nameIndex;
|
||||
return UniqName;
|
||||
}
|
||||
|
||||
deleteService(index) {
|
||||
this.formValues.Services.splice(index, 1);
|
||||
this.state.nameIndex -= 1;
|
||||
}
|
||||
|
||||
addPort(index) {
|
||||
const p = new KubernetesServicePort();
|
||||
this.formValues.Services[index].Ports.push(p);
|
||||
}
|
||||
|
||||
serviceType(type) {
|
||||
switch (type) {
|
||||
case KubernetesApplicationPublishingTypes.CLUSTER_IP:
|
||||
return KubernetesServiceTypes.CLUSTER_IP;
|
||||
case KubernetesApplicationPublishingTypes.NODE_PORT:
|
||||
return KubernetesServiceTypes.NODE_PORT;
|
||||
case KubernetesApplicationPublishingTypes.LOAD_BALANCER:
|
||||
return KubernetesServiceTypes.LOAD_BALANCER;
|
||||
case KubernetesApplicationPublishingTypes.INGRESS:
|
||||
return KubernetesServiceTypes.INGRESS;
|
||||
}
|
||||
}
|
||||
|
||||
isAdmin() {
|
||||
return this.Authentication.isAdmin();
|
||||
}
|
||||
|
||||
iconStyle(type) {
|
||||
switch (type) {
|
||||
case KubernetesApplicationPublishingTypes.CLUSTER_IP:
|
||||
return 'fa fa-list-alt';
|
||||
case KubernetesApplicationPublishingTypes.NODE_PORT:
|
||||
return 'fa fa-list';
|
||||
case KubernetesApplicationPublishingTypes.LOAD_BALANCER:
|
||||
return 'fa fa-project-diagram';
|
||||
case KubernetesApplicationPublishingTypes.INGRESS:
|
||||
return 'fa fa-route';
|
||||
}
|
||||
}
|
||||
$onInit() {
|
||||
this.state = {
|
||||
serviceType: [
|
||||
{
|
||||
typeName: KubernetesServiceTypes.CLUSTER_IP,
|
||||
typeValue: KubernetesApplicationPublishingTypes.CLUSTER_IP,
|
||||
},
|
||||
{
|
||||
typeName: KubernetesServiceTypes.NODE_PORT,
|
||||
typeValue: KubernetesApplicationPublishingTypes.NODE_PORT,
|
||||
},
|
||||
{
|
||||
typeName: KubernetesServiceTypes.LOAD_BALANCER,
|
||||
typeValue: KubernetesApplicationPublishingTypes.LOAD_BALANCER,
|
||||
},
|
||||
{
|
||||
typeName: KubernetesServiceTypes.INGRESS,
|
||||
typeValue: KubernetesApplicationPublishingTypes.INGRESS,
|
||||
},
|
||||
],
|
||||
selected: KubernetesApplicationPublishingTypes.CLUSTER_IP,
|
||||
nameIndex: this.formValues.Services.length,
|
||||
endpointId: this.EndpointProvider.endpointID(),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
<div class="col-sm-12 form-section-title">
|
||||
Publishing the application
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 form-inline">
|
||||
<div class="col-sm-5" style="padding-left: 0px;">
|
||||
<select class="form-control" ng-model="$ctrl.state.selected" ng-options="item.typeValue as item.typeName for item in $ctrl.state.serviceType"></select>
|
||||
<button type="button" class="btn btn-sm btn-default" style="margin-left: 0;" ng-click="$ctrl.addEntry( $ctrl.state.selected )" data-cy="k8sConfigCreate-createEntryButton">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> Create service
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 20px;" ng-repeat="service in $ctrl.formValues.Services">
|
||||
<div ng-if="!$ctrl.formValues.Services[$index].Ingress">
|
||||
<div class="text-muted">
|
||||
<i class="{{ $ctrl.iconStyle(service.Type) }}" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ $ctrl.serviceType(service.Type) }}
|
||||
</div>
|
||||
<kube-services-item-view
|
||||
service-routes="$ctrl.formValues.Services[$index].IngressRoute"
|
||||
ingress-type="$ctrl.formValues.Services[$index].Ingress"
|
||||
service-type="$ctrl.formValues.Services[$index].Type"
|
||||
service-ports="$ctrl.formValues.Services[$index].Ports"
|
||||
is-edit="$ctrl.isEdit"
|
||||
loadbalancer-enabled="$ctrl.loadbalancerEnabled"
|
||||
></kube-services-item-view>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger space-right"
|
||||
style="margin-left: 0; margin-top: 10px;"
|
||||
ng-click="$ctrl.deleteService( $index )"
|
||||
data-cy="k8sConfigCreate-removeButton"
|
||||
>
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.formValues.Services[$index].Ingress && $ctrl.formValues.OriginalIngresses.length === 0">
|
||||
<div class="text-muted">
|
||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Ingress
|
||||
</div>
|
||||
<div ng-if="$ctrl.isAdmin()" class="small text-warning">
|
||||
<p style="margin-top: 10px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Ingress is not configured in this namespace, select another namespace or click
|
||||
<a ui-sref="portainer.k8sendpoint.kubernetesConfig({id: $ctrl.state.endpointId})">here</a> to configure ingress.
|
||||
</p>
|
||||
</div>
|
||||
<div ng-if="!$ctrl.isAdmin()" class="small text-warning">
|
||||
<p style="margin-top: 10px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Ingress is not configured in this namespace, select another namespace or contact your administrator.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger space-right"
|
||||
style="margin-left: 0; margin-top: 10px;"
|
||||
ng-click="$ctrl.deleteService( $index )"
|
||||
data-cy="k8sConfigCreate-removeButton"
|
||||
>
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ng-if="$ctrl.formValues.Services[$index].Ingress && $ctrl.formValues.OriginalIngresses.length !== 0">
|
||||
<div class="text-muted">
|
||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Ingress
|
||||
</div>
|
||||
<kube-services-item-view
|
||||
original-ingresses="$ctrl.formValues.OriginalIngresses"
|
||||
service-routes="$ctrl.formValues.Services[$index].IngressRoute"
|
||||
ingress-type="$ctrl.formValues.Services[$index].Ingress"
|
||||
service-type="$ctrl.formValues.Services[$index].Type"
|
||||
service-ports="$ctrl.formValues.Services[$index].Ports"
|
||||
service-name="$ctrl.formValues.Services[$index].Name"
|
||||
multi-item-disable="true"
|
||||
></kube-services-item-view>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger space-right"
|
||||
style="margin-left: 0; margin-top: 10px;"
|
||||
ng-click="$ctrl.deleteService( $index )"
|
||||
data-cy="k8sConfigCreate-removeButton"
|
||||
>
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
import angular from 'angular';
|
||||
import controller from './kube-services.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubeServicesView', {
|
||||
templateUrl: './kube-services.html',
|
||||
controller,
|
||||
bindings: {
|
||||
formValues: '=',
|
||||
isEdit: '<',
|
||||
loadbalancerEnabled: '<',
|
||||
},
|
||||
});
|
|
@ -120,6 +120,8 @@ class KubernetesApplicationConverter {
|
|||
res.ServiceType = serviceType;
|
||||
res.ServiceId = service.metadata.uid;
|
||||
res.ServiceName = service.metadata.name;
|
||||
res.ClusterIp = service.spec.clusterIP;
|
||||
res.ExternalIp = service.spec.externalIP;
|
||||
|
||||
if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) {
|
||||
if (service.status.loadBalancer.ingress && service.status.loadBalancer.ingress.length > 0) {
|
||||
|
@ -279,6 +281,8 @@ class KubernetesApplicationConverter {
|
|||
res.ApplicationType = app.ApplicationType;
|
||||
res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]);
|
||||
res.Name = app.Name;
|
||||
res.Services = KubernetesApplicationHelper.generateServicesFormValuesFromServices(app);
|
||||
res.Selector = KubernetesApplicationHelper.generateSelectorFromService(app);
|
||||
res.StackName = app.StackName;
|
||||
res.ApplicationOwner = app.ApplicationOwner;
|
||||
res.ImageModel.Image = app.Image;
|
||||
|
@ -356,7 +360,9 @@ class KubernetesApplicationConverter {
|
|||
service = undefined;
|
||||
}
|
||||
|
||||
return [app, headlessService, service, claims];
|
||||
let services = KubernetesServiceConverter.applicationFormValuesToServices(formValues);
|
||||
|
||||
return [app, headlessService, services, service, claims];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,10 +52,59 @@ class KubernetesServiceConverter {
|
|||
return res;
|
||||
}
|
||||
|
||||
static applicationFormValuesToServices(formValues) {
|
||||
let services = [];
|
||||
formValues.Services.forEach(function (service) {
|
||||
const res = new KubernetesService();
|
||||
res.Namespace = formValues.ResourcePool.Namespace.Name;
|
||||
res.Name = service.Name;
|
||||
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
|
||||
res.ApplicationOwner = formValues.ApplicationOwner;
|
||||
res.ApplicationName = formValues.Name;
|
||||
if (service.Type === KubernetesApplicationPublishingTypes.NODE_PORT) {
|
||||
res.Type = KubernetesServiceTypes.NODE_PORT;
|
||||
} else if (service.Type === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
|
||||
res.Type = KubernetesServiceTypes.LOAD_BALANCER;
|
||||
} else if (service.Type === KubernetesApplicationPublishingTypes.CLUSTER_IP) {
|
||||
res.Type = KubernetesServiceTypes.CLUSTER_IP;
|
||||
}
|
||||
res.Ingress = service.Ingress;
|
||||
|
||||
if (service.Selector !== undefined) {
|
||||
res.Selector = service.Selector;
|
||||
} else {
|
||||
res.Selector = {
|
||||
app: formValues.Name,
|
||||
};
|
||||
}
|
||||
|
||||
let ports = [];
|
||||
service.Ports.forEach(function (port, index) {
|
||||
const res = new KubernetesServicePort();
|
||||
res.name = 'port-' + index;
|
||||
res.port = port.port;
|
||||
if (port.nodePort) {
|
||||
res.nodePort = port.nodePort;
|
||||
}
|
||||
res.protocol = port.protocol;
|
||||
res.targetPort = port.targetPort;
|
||||
res.ingress = port.ingress;
|
||||
ports.push(res);
|
||||
});
|
||||
res.Ports = ports;
|
||||
|
||||
services.push(res);
|
||||
});
|
||||
return services;
|
||||
}
|
||||
|
||||
static applicationFormValuesToHeadlessService(formValues) {
|
||||
const res = KubernetesServiceConverter.applicationFormValuesToService(formValues);
|
||||
res.Name = KubernetesServiceHelper.generateHeadlessServiceName(formValues.Name);
|
||||
res.Headless = true;
|
||||
res.Selector = {
|
||||
app: formValues.Name,
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -70,8 +119,20 @@ class KubernetesServiceConverter {
|
|||
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName;
|
||||
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName;
|
||||
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner;
|
||||
payload.spec.ports = service.Ports;
|
||||
payload.spec.selector.app = service.ApplicationName;
|
||||
|
||||
const ports = [];
|
||||
service.Ports.forEach((port) => {
|
||||
const p = {};
|
||||
p.name = port.name;
|
||||
p.port = port.port;
|
||||
p.nodePort = port.nodePort;
|
||||
p.protocol = port.protocol;
|
||||
p.targetPort = port.targetPort;
|
||||
ports.push(p);
|
||||
});
|
||||
payload.spec.ports = ports;
|
||||
|
||||
payload.spec.selector = service.Selector;
|
||||
if (service.Headless) {
|
||||
payload.spec.clusterIP = KubernetesServiceHeadlessClusterIP;
|
||||
delete payload.spec.ports;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import _ from 'lodash-es';
|
||||
import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models';
|
||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||
import {
|
||||
KubernetesApplicationAutoScalerFormValue,
|
||||
|
@ -276,6 +276,61 @@ class KubernetesApplicationHelper {
|
|||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region SERVICES -> SERVICES FORM VALUES */
|
||||
static generateServicesFormValuesFromServices(app) {
|
||||
let services = [];
|
||||
app.Services.forEach(function (service) {
|
||||
const svc = new KubernetesService();
|
||||
svc.Namespace = service.metadata.namespace;
|
||||
svc.Name = service.metadata.name;
|
||||
svc.StackName = service.StackName;
|
||||
svc.ApplicationOwner = app.ApplicationOwner;
|
||||
svc.ApplicationName = app.ApplicationName;
|
||||
svc.Type = service.spec.type;
|
||||
if (service.spec.type === KubernetesServiceTypes.CLUSTER_IP) {
|
||||
svc.Type = 1;
|
||||
} else if (service.spec.type === KubernetesServiceTypes.NODE_PORT) {
|
||||
svc.Type = 2;
|
||||
} else if (service.spec.type === KubernetesServiceTypes.LOAD_BALANCER) {
|
||||
svc.Type = 3;
|
||||
}
|
||||
|
||||
let ports = [];
|
||||
service.spec.ports.forEach(function (port) {
|
||||
const svcport = new KubernetesServicePort();
|
||||
svcport.name = port.name;
|
||||
svcport.port = port.port;
|
||||
svcport.nodePort = port.nodePort;
|
||||
svcport.protocol = port.protocol;
|
||||
svcport.targetPort = port.targetPort;
|
||||
|
||||
app.Ingresses.value.forEach((ingress) => {
|
||||
const ingressMatched = _.find(ingress.Paths, { ServiceName: service.metadata.name });
|
||||
if (ingressMatched) {
|
||||
svcport.ingress = {
|
||||
IngressName: ingressMatched.IngressName,
|
||||
Host: ingressMatched.Host,
|
||||
Path: ingressMatched.Path,
|
||||
};
|
||||
svc.Ingress = true;
|
||||
}
|
||||
});
|
||||
|
||||
ports.push(svcport);
|
||||
});
|
||||
svc.Ports = ports;
|
||||
svc.Selector = app.Raw.spec.selector.matchLabels;
|
||||
services.push(svc);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
/* #endregion */
|
||||
static generateSelectorFromService(app) {
|
||||
const selector = app.Raw.spec.selector.matchLabels;
|
||||
return selector;
|
||||
}
|
||||
|
||||
/* #region PUBLISHED PORTS FV <> PUBLISHED PORTS */
|
||||
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts, ingress) {
|
||||
const generatePort = (port, rule) => {
|
||||
|
|
|
@ -12,5 +12,12 @@ class KubernetesServiceHelper {
|
|||
}
|
||||
return _.find(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector));
|
||||
}
|
||||
|
||||
static findApplicationBoundServices(services, rawApp) {
|
||||
if (!rawApp.spec.template) {
|
||||
return undefined;
|
||||
}
|
||||
return _.filter(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector));
|
||||
}
|
||||
}
|
||||
export default KubernetesServiceHelper;
|
||||
|
|
|
@ -76,6 +76,69 @@ export class KubernetesIngressConverter {
|
|||
return ingresses;
|
||||
}
|
||||
|
||||
static applicationFormValuesToDeleteIngresses(formValues, application) {
|
||||
const ingresses = angular.copy(formValues.OriginalIngresses);
|
||||
application.Services.forEach((service) => {
|
||||
ingresses.forEach((ingress) => {
|
||||
const path = _.find(ingress.Paths, { ServiceName: service.metadata.name });
|
||||
if (path) {
|
||||
_.remove(ingress.Paths, path);
|
||||
}
|
||||
});
|
||||
});
|
||||
return ingresses;
|
||||
}
|
||||
|
||||
static deleteIngressByServiceName(formValues, service) {
|
||||
const ingresses = angular.copy(formValues.OriginalIngresses);
|
||||
ingresses.forEach((ingress) => {
|
||||
const path = _.find(ingress.Paths, { ServiceName: service.Name });
|
||||
if (path) {
|
||||
_.remove(ingress.Paths, path);
|
||||
}
|
||||
});
|
||||
return ingresses;
|
||||
}
|
||||
|
||||
static newApplicationFormValuesToIngresses(formValues, serviceName, servicePorts) {
|
||||
const ingresses = angular.copy(formValues.OriginalIngresses);
|
||||
servicePorts.forEach((port) => {
|
||||
const ingress = _.find(ingresses, { Name: port.ingress.IngressName });
|
||||
if (ingress) {
|
||||
const rule = new KubernetesIngressRule();
|
||||
rule.ServiceName = serviceName;
|
||||
rule.IngressName = port.ingress.IngressName;
|
||||
rule.Host = port.ingress.Host;
|
||||
rule.Path = _.startsWith(port.ingress.Path, '/') ? port.ingress.Path : '/' + port.ingress.Path;
|
||||
rule.Port = port.port;
|
||||
|
||||
ingress.Paths.push(rule);
|
||||
}
|
||||
});
|
||||
return ingresses;
|
||||
}
|
||||
|
||||
static editingFormValuesToIngresses(formValues, serviceName, servicePorts) {
|
||||
const ingresses = angular.copy(formValues.OriginalIngresses);
|
||||
servicePorts.forEach((port) => {
|
||||
const ingressMatched = _.find(ingresses, { Name: port.ingress.IngressName });
|
||||
if (ingressMatched) {
|
||||
const pathMatched = _.find(ingressMatched.Paths, { ServiceName: serviceName });
|
||||
_.remove(ingressMatched.Paths, pathMatched);
|
||||
|
||||
const rule = new KubernetesIngressRule();
|
||||
rule.ServiceName = serviceName;
|
||||
rule.IngressName = port.ingress.IngressName;
|
||||
rule.Host = port.ingress.Host;
|
||||
rule.Path = _.startsWith(port.ingress.Path, '/') ? port.ingress.Path : '/' + port.ingress.Path;
|
||||
rule.Port = port.port;
|
||||
|
||||
ingressMatched.Paths.push(rule);
|
||||
}
|
||||
});
|
||||
return ingresses;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {KubernetesResourcePoolIngressClassFormValue[]} formValues
|
||||
|
|
|
@ -18,6 +18,7 @@ export function KubernetesApplicationFormValues() {
|
|||
this.ReplicaCount = 1;
|
||||
this.AutoScaler = {};
|
||||
this.Containers = [];
|
||||
this.Services = [];
|
||||
this.EnvironmentVariables = []; // KubernetesApplicationEnvironmentVariableFormValue lis;
|
||||
this.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;
|
||||
this.PersistedFolders = []; // KubernetesApplicationPersistedFolderFormValue lis;
|
||||
|
|
|
@ -4,6 +4,7 @@ export const KubernetesServiceTypes = Object.freeze({
|
|||
LOAD_BALANCER: 'LoadBalancer',
|
||||
NODE_PORT: 'NodePort',
|
||||
CLUSTER_IP: 'ClusterIP',
|
||||
INGRESS: 'Ingress',
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -20,6 +21,8 @@ const _KubernetesService = Object.freeze({
|
|||
ApplicationName: '',
|
||||
ApplicationOwner: '',
|
||||
Note: '',
|
||||
Ingress: false,
|
||||
Selector: {},
|
||||
});
|
||||
|
||||
export class KubernetesService {
|
||||
|
@ -28,6 +31,40 @@ export class KubernetesService {
|
|||
}
|
||||
}
|
||||
|
||||
const _KubernetesIngressService = Object.freeze({
|
||||
Headless: false,
|
||||
Namespace: '',
|
||||
Name: '',
|
||||
StackName: '',
|
||||
Ports: [],
|
||||
Type: '',
|
||||
ClusterIP: '',
|
||||
ApplicationName: '',
|
||||
ApplicationOwner: '',
|
||||
Note: '',
|
||||
Ingress: true,
|
||||
IngressRoute: [],
|
||||
});
|
||||
|
||||
export class KubernetesIngressService {
|
||||
constructor() {
|
||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesIngressService)));
|
||||
}
|
||||
}
|
||||
|
||||
const _KubernetesIngressServiceRoute = Object.freeze({
|
||||
Host: '',
|
||||
IngressName: '',
|
||||
Path: '',
|
||||
ServiceName: '',
|
||||
});
|
||||
|
||||
export class KubernetesIngressServiceRoute {
|
||||
constructor() {
|
||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesIngressServiceRoute)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KubernetesServicePort Model
|
||||
*/
|
||||
|
@ -37,6 +74,7 @@ const _KubernetesServicePort = Object.freeze({
|
|||
targetPort: 0,
|
||||
protocol: '',
|
||||
nodePort: 0,
|
||||
ingress: '',
|
||||
});
|
||||
|
||||
export class KubernetesServicePort {
|
||||
|
|
|
@ -120,16 +120,19 @@ class KubernetesApplicationService {
|
|||
const services = await this.KubernetesServiceService.get(namespace);
|
||||
const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw);
|
||||
const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {};
|
||||
const boundServices = KubernetesServiceHelper.findApplicationBoundServices(services, rootItem.value.Raw);
|
||||
|
||||
const application = converterFunc(rootItem.value.Raw, pods.value, service.Raw, ingresses.value);
|
||||
application.Yaml = rootItem.value.Yaml;
|
||||
application.Raw = rootItem.value.Raw;
|
||||
application.Pods = _.map(application.Pods, (item) => KubernetesPodConverter.apiToModel(item));
|
||||
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
|
||||
application.Services = boundServices;
|
||||
|
||||
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application);
|
||||
const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(namespace, boundScaler.Name) : undefined;
|
||||
application.AutoScaler = scaler;
|
||||
application.Ingresses = ingresses;
|
||||
|
||||
await this.KubernetesHistoryService.get(application);
|
||||
|
||||
|
@ -149,8 +152,10 @@ class KubernetesApplicationService {
|
|||
|
||||
const convertToApplication = (item, converterFunc, services, pods, ingresses) => {
|
||||
const service = KubernetesServiceHelper.findApplicationBoundService(services, item);
|
||||
const servicesFound = KubernetesServiceHelper.findApplicationBoundServices(services, item);
|
||||
const application = converterFunc(item, pods, service, ingresses);
|
||||
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
|
||||
application.Services = servicesFound;
|
||||
return application;
|
||||
};
|
||||
|
||||
|
@ -187,6 +192,7 @@ class KubernetesApplicationService {
|
|||
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application);
|
||||
const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(ns, boundScaler.Name) : undefined;
|
||||
application.AutoScaler = scaler;
|
||||
application.Ingresses = await this.KubernetesIngressService.get(ns);
|
||||
})
|
||||
);
|
||||
return applications;
|
||||
|
@ -214,7 +220,18 @@ class KubernetesApplicationService {
|
|||
* also be displayed in the summary output (getCreatedApplicationResources)
|
||||
*/
|
||||
async createAsync(formValues) {
|
||||
let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
||||
// formValues -> Application
|
||||
let [app, headlessService, services, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
||||
|
||||
if (services) {
|
||||
services.forEach(async (service) => {
|
||||
this.KubernetesServiceService.create(service);
|
||||
if (service.Ingress) {
|
||||
const ingresses = KubernetesIngressConverter.newApplicationFormValuesToIngresses(formValues, service.Name, service.Ports);
|
||||
await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (service) {
|
||||
await this.KubernetesServiceService.create(service);
|
||||
|
@ -261,8 +278,8 @@ class KubernetesApplicationService {
|
|||
* in this method should also be displayed in the summary output (getUpdatedApplicationResources)
|
||||
*/
|
||||
async patchAsync(oldFormValues, newFormValues) {
|
||||
const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
|
||||
const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
|
||||
const [oldApp, oldHeadlessService, oldServices, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
|
||||
const [newApp, newHeadlessService, newServices, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
|
||||
const oldApiService = this._getApplicationApiService(oldApp);
|
||||
const newApiService = this._getApplicationApiService(newApp);
|
||||
|
||||
|
@ -271,6 +288,9 @@ class KubernetesApplicationService {
|
|||
if (oldService) {
|
||||
await this.KubernetesServiceService.delete(oldService);
|
||||
}
|
||||
if (newService) {
|
||||
return '';
|
||||
}
|
||||
return await this.create(newFormValues);
|
||||
}
|
||||
|
||||
|
@ -290,25 +310,54 @@ class KubernetesApplicationService {
|
|||
|
||||
await newApiService.patch(oldApp, newApp);
|
||||
|
||||
if (oldService && newService) {
|
||||
await this.KubernetesServiceService.patch(oldService, newService);
|
||||
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const oldIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(oldFormValues, oldService.Name);
|
||||
const newIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(oldIngresses, newIngresses));
|
||||
}
|
||||
} else if (!oldService && newService) {
|
||||
await this.KubernetesServiceService.create(newService);
|
||||
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(newFormValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
} else if (oldService && !newService) {
|
||||
await this.KubernetesServiceService.delete(oldService);
|
||||
if (oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, oldService.Name);
|
||||
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
if (oldServices.length === 0 && newServices.length !== 0) {
|
||||
newServices.forEach(async (service) => {
|
||||
await this.KubernetesServiceService.create(service);
|
||||
if (service.Ingress) {
|
||||
const ingresses = KubernetesIngressConverter.newApplicationFormValuesToIngresses(oldFormValues, service.Name, service.Ports);
|
||||
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (oldServices.length !== 0 && newServices.length === 0) {
|
||||
oldServices.forEach(async (oldService) => {
|
||||
if (oldService.Ingress) {
|
||||
const ingresses = KubernetesIngressConverter.deleteIngressByServiceName(oldFormValues, oldService);
|
||||
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
});
|
||||
await this.KubernetesServiceService.deleteAll(oldServices);
|
||||
}
|
||||
|
||||
if (oldServices.length !== 0 && newServices.length !== 0) {
|
||||
newServices.forEach(async (newService) => {
|
||||
const oldServiceMatched = _.find(oldServices, { Name: newService.Name });
|
||||
if (oldServiceMatched) {
|
||||
await this.KubernetesServiceService.patch(oldServiceMatched, newService);
|
||||
if (newService.Ingress) {
|
||||
const ingresses = KubernetesIngressConverter.editingFormValuesToIngresses(oldFormValues, newService.Name, newService.Ports);
|
||||
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
} else {
|
||||
await this.KubernetesServiceService.create(newService);
|
||||
if (newService.Ingress) {
|
||||
const ingresses = KubernetesIngressConverter.newApplicationFormValuesToIngresses(oldFormValues, newService.Name, newService.Ports);
|
||||
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
oldServices.forEach(async (oldService) => {
|
||||
const newServiceMatched = _.find(newServices, { Name: oldService.Name });
|
||||
if (!newServiceMatched) {
|
||||
await this.KubernetesServiceService.deleteSingle(oldService);
|
||||
if (oldService.Ingress) {
|
||||
const ingresses = KubernetesIngressConverter.deleteIngressByServiceName(oldFormValues, oldService);
|
||||
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp);
|
||||
|
@ -381,16 +430,16 @@ class KubernetesApplicationService {
|
|||
}
|
||||
|
||||
if (application.ServiceType) {
|
||||
await this.KubernetesServiceService.delete(servicePayload);
|
||||
const isIngress = _.filter(application.PublishedPorts, (p) => p.IngressRules.length).length;
|
||||
if (isIngress) {
|
||||
await this.KubernetesServiceService.delete(application.Services);
|
||||
|
||||
if (application.Ingresses.length) {
|
||||
const originalIngresses = await this.KubernetesIngressService.get(payload.Namespace);
|
||||
const formValues = {
|
||||
OriginalIngresses: originalIngresses,
|
||||
PublishedPorts: KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(application.ServiceType, application.PublishedPorts),
|
||||
};
|
||||
_.forEach(formValues.PublishedPorts, (p) => (p.NeedsDeletion = true));
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, servicePayload.Name);
|
||||
const ingresses = KubernetesIngressConverter.applicationFormValuesToDeleteIngresses(formValues, application);
|
||||
|
||||
await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ class KubernetesServiceService {
|
|||
this.createAsync = this.createAsync.bind(this);
|
||||
this.patchAsync = this.patchAsync.bind(this);
|
||||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
this.deleteSingleAsync = this.deleteSingleAsync.bind(this);
|
||||
this.deleteAllAsync = this.deleteAllAsync.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,7 +97,43 @@ class KubernetesServiceService {
|
|||
/**
|
||||
* DELETE
|
||||
*/
|
||||
async deleteAsync(service) {
|
||||
async deleteAsync(services) {
|
||||
services.forEach(async (service) => {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = service.metadata.name;
|
||||
const namespace = service.metadata.namespace;
|
||||
await this.KubernetesServices(namespace).delete(params).$promise;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('unable to remove service', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
delete(services) {
|
||||
return this.$async(this.deleteAsync, services);
|
||||
}
|
||||
|
||||
async deleteAllAsync(formValuesServices) {
|
||||
formValuesServices.forEach(async (service) => {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = service.Name;
|
||||
const namespace = service.Namespace;
|
||||
await this.KubernetesServices(namespace).delete(params).$promise;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('unable to remove service', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteAll(formValuesServices) {
|
||||
return this.$async(this.deleteAllAsync, formValuesServices);
|
||||
}
|
||||
|
||||
async deleteSingleAsync(service) {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = service.Name;
|
||||
|
@ -107,8 +145,8 @@ class KubernetesServiceService {
|
|||
}
|
||||
}
|
||||
|
||||
delete(service) {
|
||||
return this.$async(this.deleteAsync, service);
|
||||
deleteSingle(service) {
|
||||
return this.$async(this.deleteSingleAsync, service);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -720,7 +720,7 @@ class KubernetesCreateApplicationController {
|
|||
}
|
||||
|
||||
publishViaLoadBalancerEnabled() {
|
||||
return this.state.useLoadBalancer;
|
||||
return this.state.useLoadBalancer && this.state.maxLoadBalancersQuota !== 0;
|
||||
}
|
||||
|
||||
publishViaIngressEnabled() {
|
||||
|
@ -799,6 +799,14 @@ class KubernetesCreateApplicationController {
|
|||
return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable || isPublishingWithoutPorts;
|
||||
}
|
||||
|
||||
isExternalApplication() {
|
||||
if (this.application) {
|
||||
return KubernetesApplicationHelper.isExternalApplication(this.application);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
disableLoadBalancerEdit() {
|
||||
return (
|
||||
this.state.isEdit &&
|
||||
|
|
|
@ -208,6 +208,17 @@
|
|||
>
|
||||
<i class="fa fa-file-code space-right" aria-hidden="true"></i>Edit this application
|
||||
</button>
|
||||
<button
|
||||
authorization="K8sApplicationDetailsW"
|
||||
ng-if="ctrl.isExternalApplication()"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
ui-sref="kubernetes.applications.application.edit"
|
||||
style="margin-left: 0;"
|
||||
data-cy="k8sAppDetail-editAppButton"
|
||||
>
|
||||
<i class="fa fa-file-code space-right" aria-hidden="true"></i>Edit External application
|
||||
</button>
|
||||
<button
|
||||
ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD"
|
||||
type="button"
|
||||
|
@ -249,147 +260,27 @@
|
|||
</div>
|
||||
|
||||
<div ng-if="ctrl.application.PublishedPorts.length > 0">
|
||||
<!-- 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 a service of type <span class="bold">{{ ctrl.application.ServiceType }}</span
|
||||
>. Refer to the port configuration below to access it.
|
||||
</div>
|
||||
<div style="margin-top: 10px;" class="small text-muted">
|
||||
<span ng-if="!ctrl.application.LoadBalancerIPAddress">
|
||||
<p> Load balancer status: <i class="fa fa-cog fa-spin" style="margin-left: 2px;"></i> pending </p>
|
||||
<p>
|
||||
<u>what does the "pending" status means?</u>
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="A pending status means that Portainer delegated the request to the provider responsible for creating the external load balancer. If it stays in pending state for long, this means that this capability might not be supported or you might have an issue with your cluster provider. Contact your cluster administrator for more information."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</p>
|
||||
</span>
|
||||
<span ng-if="ctrl.application.LoadBalancerIPAddress">
|
||||
<p> Load balancer status: <i class="fa fa-check green-icon" style="margin-left: 2px;"></i> available </p>
|
||||
<p>
|
||||
Load balancer IP address: {{ ctrl.application.LoadBalancerIPAddress }}
|
||||
<span class="btn btn-primary btn-xs" ng-click="ctrl.copyLoadBalancerIP()" style="margin-left: 5px;">
|
||||
<i class="fa fa-copy space-right" aria-hidden="true"></i>Copy
|
||||
</span>
|
||||
<span id="copyNotificationLB" style="margin-left: 7px; display: none; color: #23ae89;" class="small">
|
||||
<i class="fa fa-check" aria-hidden="true"></i> copied
|
||||
</span>
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NodePort 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 through a service of type <span class="bold">{{ ctrl.application.ServiceType }}</span
|
||||
>. It can be reached using the IP address of any node in your cluster using the port configuration below.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ClusterIP notice -->
|
||||
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.CLUSTER_IP && !ctrl.state.useIngress">
|
||||
<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 a service of type <span class="bold">{{ ctrl.application.ServiceType }}</span
|
||||
>. It can be reached via the application name <code>{{ ctrl.application.ServiceName }}</code> and the port configuration below.
|
||||
<span class="btn btn-primary btn-xs" ng-click="ctrl.copyApplicationName()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</span>
|
||||
<span id="copyNotificationApplicationName" style="margin-left: 7px; display: none; color: #23ae89;" class="small"
|
||||
><i class="fa fa-check" aria-hidden="true"></i> copied</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ingress notice -->
|
||||
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.CLUSTER_IP && ctrl.state.useIngress">
|
||||
<!-- Services notice -->
|
||||
<div>
|
||||
<div class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application is exposed through a service of type <span class="bold">{{ ctrl.application.ServiceType }}</span
|
||||
>. It can be reached via the application name <code>{{ ctrl.application.ServiceName }}</code> and the port configuration below.
|
||||
<span class="btn btn-primary btn-xs" ng-click="ctrl.copyApplicationName()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</span>
|
||||
<span id="copyNotificationApplicationName" style="margin-left: 7px; display: none; color: #23ae89;" class="small"
|
||||
><i class="fa fa-check" aria-hidden="true"></i> copied</span
|
||||
>
|
||||
This application is exposed through service(s) as below:
|
||||
</p>
|
||||
<p>It is also associated to an <span class="bold">Ingress</span> and can be accessed via specific HTTP route(s).</p>
|
||||
</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%;">Service 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)" data-cy="k8sAppDetail-containerPort">{{ port.TargetPort }}/{{ port.Protocol }}</td>
|
||||
<td ng-if="!ctrl.portHasIngressRules(port)">
|
||||
<span ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-nodePort">
|
||||
{{ port.NodePort }}
|
||||
</span>
|
||||
<span ng-if="ctrl.application.ServiceType !== ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-containerPort">
|
||||
{{ port.Port }}
|
||||
</span>
|
||||
<a
|
||||
ng-if="ctrl.application.LoadBalancerIPAddress"
|
||||
ng-href="http://{{ ctrl.application.LoadBalancerIPAddress }}:{{ port.Port }}"
|
||||
target="_blank"
|
||||
style="margin-left: 5px;"
|
||||
data-cy="k8sAppDetail-accessLink"
|
||||
>
|
||||
<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 data-cy="k8sAppDetail-httpRoute">{{ port.TargetPort }}/{{ port.Protocol }}</td>
|
||||
<td>
|
||||
<span ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-nodePort">
|
||||
{{ port.NodePort }}
|
||||
</span>
|
||||
<span ng-if="ctrl.application.ServiceType !== ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-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" data-cy="k8sAppDetail-httpRouteLink">
|
||||
{{ ctrl.buildIngressRuleURL(rule) | stripprotocol }}
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<kubernetes-application-services-table
|
||||
services="ctrl.application.Services"
|
||||
application="ctrl.application"
|
||||
public-url="ctrl.state.publicUrl"
|
||||
></kubernetes-application-services-table>
|
||||
<!-- table -->
|
||||
|
||||
<!-- table -->
|
||||
<kubernetes-application-ingress-table application="ctrl.application" public-url="ctrl.state.publicUrl"></kubernetes-application-ingress-table>
|
||||
<!-- table -->
|
||||
</div>
|
||||
<!-- !ACCESSING APPLICATION -->
|
||||
<!-- AUTO SCALING -->
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
import {
|
||||
KubernetesApplicationDataAccessPolicies,
|
||||
KubernetesApplicationDeploymentTypes,
|
||||
|
@ -112,7 +113,6 @@ class KubernetesApplicationController {
|
|||
KubernetesStackService,
|
||||
KubernetesPodService,
|
||||
KubernetesNodeService,
|
||||
|
||||
StackService
|
||||
) {
|
||||
this.$async = $async;
|
||||
|
@ -317,6 +317,7 @@ class KubernetesApplicationController {
|
|||
this.application = application;
|
||||
this.allContainers = KubernetesApplicationHelper.associateAllContainersAndApplication(application);
|
||||
this.formValues.Note = this.application.Note;
|
||||
this.formValues.Services = this.application.Services;
|
||||
if (this.application.Note) {
|
||||
this.state.expandedNote = true;
|
||||
}
|
||||
|
@ -347,6 +348,12 @@ class KubernetesApplicationController {
|
|||
}
|
||||
|
||||
async onInit() {
|
||||
const endpointId = this.LocalStorage.getEndpointID();
|
||||
const endpoints = this.LocalStorage.getEndpoints();
|
||||
const endpoint = _.find(endpoints, function (item) {
|
||||
return item.Id === endpointId;
|
||||
});
|
||||
|
||||
this.state = {
|
||||
activeTab: 0,
|
||||
currentName: this.$state.$current.name,
|
||||
|
@ -365,6 +372,7 @@ class KubernetesApplicationController {
|
|||
expandedNote: false,
|
||||
useIngress: false,
|
||||
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
|
||||
publicUrl: endpoint.PublicURL,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('application');
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
export default class KubernetesApplicationIngressController {
|
||||
/* @ngInject */
|
||||
constructor($async, KubernetesIngressService) {
|
||||
this.$async = $async;
|
||||
this.KubernetesIngressService = KubernetesIngressService;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.hasIngress;
|
||||
this.applicationIngress = [];
|
||||
const ingresses = await this.KubernetesIngressService.get(this.application.ResourcePool);
|
||||
const services = this.application.Services;
|
||||
|
||||
_.forEach(services, (service) => {
|
||||
_.forEach(ingresses, (ingress) => {
|
||||
_.forEach(ingress.Paths, (path) => {
|
||||
if (path.ServiceName === service.metadata.name) {
|
||||
this.applicationIngress.push(path);
|
||||
this.hasIngress = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<div style="margin-top: 15px" ng-if="$ctrl.hasIngress">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 15%">Ingress name</td>
|
||||
<td style="width: 10%">Service name</td>
|
||||
<td style="width: 10%">Host</td>
|
||||
<td style="width: 10%">Port</td>
|
||||
<td style="width: 10%">Path</td>
|
||||
<td style="width: 15%">HTTP Route</td>
|
||||
</tr>
|
||||
<tr ng-repeat="ingress in $ctrl.applicationIngress">
|
||||
<td>{{ ingress.IngressName }}</td>
|
||||
<td>{{ ingress.ServiceName }}</td>
|
||||
<td>{{ ingress.Host }}</td>
|
||||
<td>{{ ingress.Port }}</td>
|
||||
<td>{{ ingress.Path }}</td>
|
||||
<td
|
||||
><a target="_blank" href="http://{{ ingress.Host }}{{ ingress.Path }}">{{ ingress.Host }}{{ ingress.Path }}</a></td
|
||||
>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
import angular from 'angular';
|
||||
import controller from './ingress-table.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubernetesApplicationIngressTable', {
|
||||
templateUrl: './ingress-table.html',
|
||||
controller,
|
||||
bindings: {
|
||||
application: '<',
|
||||
publicUrl: '<',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
<!-- table -->
|
||||
<div style="margin-top: 15px">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 15%">Service name</td>
|
||||
<td style="width: 10%">Type</td>
|
||||
<td style="width: 10%">Cluster IP</td>
|
||||
<td style="width: 10%">External IP</td>
|
||||
<td style="width: 10%">Container port</td>
|
||||
<td style="width: 15%">Service port(s)</td>
|
||||
</tr>
|
||||
<tr ng-repeat="service in $ctrl.services">
|
||||
<td>{{ service.metadata.name }}</td>
|
||||
<td>{{ service.spec.type }}</td>
|
||||
<td>{{ service.spec.clusterIP }}</td>
|
||||
<td ng-show="service.spec.type === 'LoadBalancer'">
|
||||
<div ng-show="service.status.loadBalancer.ingress">
|
||||
<a target="_blank" ng-href="http://{{ service.status.loadBalancer.ingress[0].ip }}:{{ service.spec.ports[0].port }}">
|
||||
<i class="fa fa-external-link-alt" aria-hidden="true"></i>
|
||||
<span data-cy="k8sAppDetail-containerPort"> Access </span>
|
||||
</a>
|
||||
</div>
|
||||
<div ng-show="!service.status.loadBalancer.ingress">
|
||||
{{ service.spec.externalIP ? service.spec.externalIP : 'pending...' }}
|
||||
</div>
|
||||
</td>
|
||||
<td ng-show="service.spec.type !== 'LoadBalancer'">{{ service.spec.externalIP ? service.spec.externalIP : '-' }}</td>
|
||||
|
||||
<td data-cy="k8sAppDetail-containerPort">
|
||||
<div ng-repeat="port in service.spec.ports">{{ port.targetPort }}</div>
|
||||
</td>
|
||||
<td ng-if="!ctrl.portHasIngressRules(port)">
|
||||
<div ng-repeat="port in service.spec.ports">
|
||||
<a ng-if="$ctrl.publicUrl && port.nodePort" ng-href="http://{{ $ctrl.publicUrl }}:{{ port.nodePort }}" target="_blank" style="margin-left: 5px">
|
||||
<i class="fa fa-external-link-alt" aria-hidden="true"></i>
|
||||
<span data-cy="k8sAppDetail-containerPort">
|
||||
{{ port.port }}
|
||||
</span>
|
||||
<span>{{ port.nodePort ? ':' : '' }}</span>
|
||||
<span data-cy="k8sAppDetail-nodePort"> {{ port.nodePort }}/{{ port.protocol }} </span>
|
||||
</a>
|
||||
|
||||
<div ng-if="!$ctrl.publicUrl">
|
||||
<span data-cy="k8sAppDetail-servicePort">
|
||||
{{ port.port }}
|
||||
</span>
|
||||
<span>{{ port.nodePort ? ':' : '' }}</span>
|
||||
<span data-cy="k8sAppDetail-nodePort"> {{ port.nodePort }}/{{ port.protocol }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubernetesApplicationServicesTable', {
|
||||
templateUrl: './services-table.html',
|
||||
bindings: {
|
||||
services: '<',
|
||||
application: '<',
|
||||
publicUrl: '<',
|
||||
},
|
||||
});
|
|
@ -4,7 +4,7 @@ import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/f
|
|||
import { KubernetesDeployment } from 'Kubernetes/models/deployment/models';
|
||||
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
|
||||
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
|
||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesService, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import {
|
||||
KubernetesApplication,
|
||||
KubernetesApplicationDeploymentTypes,
|
||||
|
@ -40,7 +40,17 @@ export default function (formValues, oldFormValues = {}) {
|
|||
function getCreatedApplicationResources(formValues) {
|
||||
const resources = [];
|
||||
|
||||
let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
||||
let [app, headlessService, services, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
||||
|
||||
if (services) {
|
||||
services.forEach((service) => {
|
||||
resources.push({ action: CREATE, kind: KubernetesResourceTypes.SERVICE, name: service.Name, type: service.Type || KubernetesServiceTypes.CLUSTER_IP });
|
||||
if (formValues.OriginalIngresses.length !== 0) {
|
||||
const ingresses = KubernetesIngressConverter.newApplicationFormValuesToIngresses(formValues, service.Name, service.Ports);
|
||||
resources.push(...getIngressUpdateSummary(formValues.OriginalIngresses, ingresses));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (service) {
|
||||
// Service
|
||||
|
@ -87,16 +97,15 @@ function getCreatedApplicationResources(formValues) {
|
|||
function getUpdatedApplicationResources(oldFormValues, newFormValues) {
|
||||
const resources = [];
|
||||
|
||||
const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
|
||||
const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
|
||||
|
||||
const [oldApp, oldHeadlessService, oldServices, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
|
||||
const [newApp, newHeadlessService, newServices, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
|
||||
const oldAppResourceType = getApplicationResourceType(oldApp);
|
||||
const newAppResourceType = getApplicationResourceType(newApp);
|
||||
|
||||
if (oldAppResourceType !== newAppResourceType) {
|
||||
// Deployment
|
||||
resources.push({ action: DELETE, kind: oldAppResourceType, name: oldApp.Name });
|
||||
if (oldService) {
|
||||
if (oldService && oldServices) {
|
||||
// Service
|
||||
resources.push({ action: DELETE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP });
|
||||
}
|
||||
|
@ -129,11 +138,13 @@ function getUpdatedApplicationResources(oldFormValues, newFormValues) {
|
|||
// Deployment
|
||||
resources.push({ action: UPDATE, kind: oldAppResourceType, name: oldApp.Name });
|
||||
|
||||
if (oldService && newService) {
|
||||
if (oldServices && newServices) {
|
||||
// Service
|
||||
const serviceUpdateResourceSummary = getServiceUpdateResourceSummary(oldService, newService);
|
||||
if (serviceUpdateResourceSummary) {
|
||||
resources.push(serviceUpdateResourceSummary);
|
||||
const serviceUpdateResourceSummary = getServiceUpdateResourceSummary(oldServices, newServices);
|
||||
if (serviceUpdateResourceSummary !== null) {
|
||||
serviceUpdateResourceSummary.forEach((updateSummary) => {
|
||||
resources.push(updateSummary);
|
||||
});
|
||||
}
|
||||
|
||||
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
|
@ -224,10 +235,41 @@ function getVolumeClaimUpdateResourceSummary(oldPVC, newPVC) {
|
|||
}
|
||||
|
||||
// getServiceUpdateResourceSummary replicates KubernetesServiceService.patch
|
||||
function getServiceUpdateResourceSummary(oldService, newService) {
|
||||
const payload = KubernetesServiceConverter.patchPayload(oldService, newService);
|
||||
if (payload.length) {
|
||||
return { action: UPDATE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP };
|
||||
function getServiceUpdateResourceSummary(oldServices, newServices) {
|
||||
let summary = [];
|
||||
newServices.forEach((newService) => {
|
||||
const oldServiceMatched = _.find(oldServices, { Name: newService.Name });
|
||||
if (oldServiceMatched) {
|
||||
const payload = KubernetesServiceConverter.patchPayload(oldServiceMatched, newService);
|
||||
if (payload.length) {
|
||||
const serviceUpdate = {
|
||||
action: UPDATE,
|
||||
kind: KubernetesResourceTypes.SERVICE,
|
||||
name: oldServiceMatched.Name,
|
||||
type: oldServiceMatched.Type || KubernetesServiceTypes.CLUSTER_IP,
|
||||
};
|
||||
summary.push(serviceUpdate);
|
||||
}
|
||||
} else {
|
||||
const emptyService = new KubernetesService();
|
||||
const payload = KubernetesServiceConverter.patchPayload(emptyService, newService);
|
||||
if (payload.length) {
|
||||
const serviceCreate = { action: CREATE, kind: KubernetesResourceTypes.SERVICE, name: newService.Name, type: newService.Type || KubernetesServiceTypes.CLUSTER_IP };
|
||||
summary.push(serviceCreate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
oldServices.forEach((oldService) => {
|
||||
const newServiceMatched = _.find(newServices, { Name: oldService.Name });
|
||||
if (!newServiceMatched) {
|
||||
const serviceDelete = { action: DELETE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP };
|
||||
summary.push(serviceDelete);
|
||||
}
|
||||
});
|
||||
|
||||
if (summary.length !== 0) {
|
||||
return summary;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue