mirror of https://github.com/portainer/portainer
refactor(app): migrate app summary section [EE-6239] (#10910)
parent
7a4314032a
commit
abf517de28
|
@ -219,7 +219,7 @@
|
||||||
<div class="menuContent">
|
<div class="menuContent">
|
||||||
<div class="md-checkbox" ng-repeat="filter in $ctrl.filters.state.values track by $index">
|
<div class="md-checkbox" ng-repeat="filter in $ctrl.filters.state.values track by $index">
|
||||||
<input id="filter_state_{{ $index }}" type="checkbox" ng-model="filter.display" ng-change="$ctrl.onStateFilterChange()" />
|
<input id="filter_state_{{ $index }}" type="checkbox" ng-model="filter.display" ng-change="$ctrl.onStateFilterChange()" />
|
||||||
<label for="filter_state_{{ $index }}">{{ filter.type | kubernetesApplicationTypeText }}</label>
|
<label for="filter_state_{{ $index }}">{{ filter.type }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -282,7 +282,7 @@
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
ng-if="!item.KubernetesApplications"
|
ng-if="!item.KubernetesApplications"
|
||||||
ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool, 'resource-type': $ctrl.applicationTypeEnumToParamMap[item.ApplicationType] })"
|
ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool, 'resource-type': item.ApplicationType })"
|
||||||
ng-click="$event.stopPropagation()"
|
ng-click="$event.stopPropagation()"
|
||||||
class="hyperlink"
|
class="hyperlink"
|
||||||
>{{ item.Name }}
|
>{{ item.Name }}
|
||||||
|
@ -297,17 +297,17 @@
|
||||||
<td title="{{ item.Image }}"
|
<td title="{{ item.Image }}"
|
||||||
>{{ item.Image | truncate: 64 }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
|
>{{ item.Image | truncate: 64 }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
|
||||||
>
|
>
|
||||||
<td>{{ item.ApplicationType | kubernetesApplicationTypeText }}</td>
|
<td>{{ item.ApplicationType }}</td>
|
||||||
<td ng-if="item.ApplicationType !== $ctrl.KubernetesApplicationTypes.POD">
|
<td ng-if="item.ApplicationType !== $ctrl.KubernetesApplicationTypes.Pod">
|
||||||
<status-indicator ok="(item.TotalPodsCount > 0 && item.TotalPodsCount === item.RunningPodsCount) || item.Status === 'Ready'"></status-indicator>
|
<status-indicator ok="(item.TotalPodsCount > 0 && item.TotalPodsCount === item.RunningPodsCount) || item.Status === 'Ready'"></status-indicator>
|
||||||
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.REPLICATED">Replicated</span>
|
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.Replicated">Replicated</span>
|
||||||
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.GLOBAL">Global</span>
|
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.Global">Global</span>
|
||||||
<span ng-if="item.RunningPodsCount >= 0 && item.TotalPodsCount >= 0"
|
<span ng-if="item.RunningPodsCount >= 0 && item.TotalPodsCount >= 0"
|
||||||
><code>{{ item.RunningPodsCount }}</code> / <code>{{ item.TotalPodsCount }}</code></span
|
><code>{{ item.RunningPodsCount }}</code> / <code>{{ item.TotalPodsCount }}</code></span
|
||||||
>
|
>
|
||||||
<span ng-if="item.KubernetesApplications">{{ item.Status }}</span>
|
<span ng-if="item.KubernetesApplications">{{ item.Status }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td ng-if="item.ApplicationType === $ctrl.KubernetesApplicationTypes.POD">
|
<td ng-if="item.ApplicationType === $ctrl.KubernetesApplicationTypes.Pod">
|
||||||
{{ item.Pods[0].Status }}
|
{{ item.Pods[0].Status }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
|
||||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||||
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
|
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
|
||||||
|
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
|
||||||
|
|
||||||
angular.module('portainer.kubernetes').controller('KubernetesApplicationsDatatableController', [
|
angular.module('portainer.kubernetes').controller('KubernetesApplicationsDatatableController', [
|
||||||
'$scope',
|
'$scope',
|
||||||
|
@ -33,13 +33,6 @@ angular.module('portainer.kubernetes').controller('KubernetesApplicationsDatatab
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.applicationTypeEnumToParamMap = {
|
|
||||||
[KubernetesApplicationTypes.DEPLOYMENT]: 'Deployment',
|
|
||||||
[KubernetesApplicationTypes.DAEMONSET]: 'DaemonSet',
|
|
||||||
[KubernetesApplicationTypes.STATEFULSET]: 'StatefulSet',
|
|
||||||
[KubernetesApplicationTypes.POD]: 'Pod',
|
|
||||||
};
|
|
||||||
|
|
||||||
this.expandAll = function () {
|
this.expandAll = function () {
|
||||||
this.state.expandAll = !this.state.expandAll;
|
this.state.expandAll = !this.state.expandAll;
|
||||||
this.state.filteredDataSet.forEach((item) => this.expandItem(item, this.state.expandAll));
|
this.state.filteredDataSet.forEach((item) => this.expandItem(item, this.state.expandAll));
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
|
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models/appConstants';
|
||||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
|
|
||||||
|
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
|
||||||
import {
|
import {
|
||||||
KubernetesApplication,
|
KubernetesApplication,
|
||||||
KubernetesApplicationConfigurationVolume,
|
KubernetesApplicationConfigurationVolume,
|
||||||
KubernetesApplicationDataAccessPolicies,
|
|
||||||
KubernetesApplicationDeploymentTypes,
|
|
||||||
KubernetesApplicationPersistedFolder,
|
KubernetesApplicationPersistedFolder,
|
||||||
KubernetesApplicationPort,
|
KubernetesApplicationPort,
|
||||||
KubernetesApplicationPublishingTypes,
|
|
||||||
KubernetesApplicationTypes,
|
|
||||||
KubernetesPortainerApplicationNameLabel,
|
KubernetesPortainerApplicationNameLabel,
|
||||||
KubernetesPortainerApplicationNote,
|
KubernetesPortainerApplicationNote,
|
||||||
KubernetesPortainerApplicationOwnerLabel,
|
KubernetesPortainerApplicationOwnerLabel,
|
||||||
|
@ -241,16 +238,16 @@ class KubernetesApplicationConverter {
|
||||||
static apiPodToApplication(data, pods, service, ingresses) {
|
static apiPodToApplication(data, pods, service, ingresses) {
|
||||||
const res = new KubernetesApplication();
|
const res = new KubernetesApplication();
|
||||||
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
|
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
|
||||||
res.ApplicationType = KubernetesApplicationTypes.POD;
|
res.ApplicationType = KubernetesApplicationTypes.Pod;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static apiDeploymentToApplication(data, pods, service, ingresses) {
|
static apiDeploymentToApplication(data, pods, service, ingresses) {
|
||||||
const res = new KubernetesApplication();
|
const res = new KubernetesApplication();
|
||||||
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
|
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
|
||||||
res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT;
|
res.ApplicationType = KubernetesApplicationTypes.Deployment;
|
||||||
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
|
res.DeploymentType = KubernetesApplicationDeploymentTypes.Replicated;
|
||||||
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
|
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.Shared;
|
||||||
res.RunningPodsCount = data.status.availableReplicas || data.status.replicas - data.status.unavailableReplicas || 0;
|
res.RunningPodsCount = data.status.availableReplicas || data.status.replicas - data.status.unavailableReplicas || 0;
|
||||||
res.TotalPodsCount = data.spec.replicas;
|
res.TotalPodsCount = data.spec.replicas;
|
||||||
return res;
|
return res;
|
||||||
|
@ -259,9 +256,9 @@ class KubernetesApplicationConverter {
|
||||||
static apiDaemonSetToApplication(data, pods, service, ingresses) {
|
static apiDaemonSetToApplication(data, pods, service, ingresses) {
|
||||||
const res = new KubernetesApplication();
|
const res = new KubernetesApplication();
|
||||||
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
|
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
|
||||||
res.ApplicationType = KubernetesApplicationTypes.DAEMONSET;
|
res.ApplicationType = KubernetesApplicationTypes.DaemonSet;
|
||||||
res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL;
|
res.DeploymentType = KubernetesApplicationDeploymentTypes.Global;
|
||||||
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
|
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.Shared;
|
||||||
res.RunningPodsCount = data.status.numberAvailable || data.status.desiredNumberScheduled - data.status.numberUnavailable || 0;
|
res.RunningPodsCount = data.status.numberAvailable || data.status.desiredNumberScheduled - data.status.numberUnavailable || 0;
|
||||||
res.TotalPodsCount = data.status.desiredNumberScheduled;
|
res.TotalPodsCount = data.status.desiredNumberScheduled;
|
||||||
return res;
|
return res;
|
||||||
|
@ -270,9 +267,9 @@ class KubernetesApplicationConverter {
|
||||||
static apiStatefulSetToapplication(data, pods, service, ingresses) {
|
static apiStatefulSetToapplication(data, pods, service, ingresses) {
|
||||||
const res = new KubernetesApplication();
|
const res = new KubernetesApplication();
|
||||||
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
|
KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
|
||||||
res.ApplicationType = KubernetesApplicationTypes.STATEFULSET;
|
res.ApplicationType = KubernetesApplicationTypes.StatefulSet;
|
||||||
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
|
res.DeploymentType = KubernetesApplicationDeploymentTypes.Replicated;
|
||||||
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;
|
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.Isolated;
|
||||||
res.RunningPodsCount = data.status.readyReplicas || 0;
|
res.RunningPodsCount = data.status.readyReplicas || 0;
|
||||||
res.TotalPodsCount = data.spec.replicas;
|
res.TotalPodsCount = data.spec.replicas;
|
||||||
res.HeadlessServiceName = data.spec.serviceName;
|
res.HeadlessServiceName = data.spec.serviceName;
|
||||||
|
@ -313,16 +310,7 @@ class KubernetesApplicationConverter {
|
||||||
res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts, ingresses);
|
res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts, ingresses);
|
||||||
res.Containers = app.Containers;
|
res.Containers = app.Containers;
|
||||||
|
|
||||||
const isIngress = _.filter(res.PublishedPorts, (p) => p.IngressName).length;
|
res.PublishingType = app.ServiceType;
|
||||||
if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) {
|
|
||||||
res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER;
|
|
||||||
} else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT) {
|
|
||||||
res.PublishingType = KubernetesApplicationPublishingTypes.NODE_PORT;
|
|
||||||
} else if (app.ServiceType === KubernetesServiceTypes.CLUSTER_IP && isIngress) {
|
|
||||||
res.PublishingType = KubernetesApplicationPublishingTypes.INGRESS;
|
|
||||||
} else {
|
|
||||||
res.PublishingType = KubernetesApplicationPublishingTypes.CLUSTER_IP;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.Pods && app.Pods.length) {
|
if (app.Pods && app.Pods.length) {
|
||||||
KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity);
|
KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity);
|
||||||
|
@ -338,20 +326,20 @@ class KubernetesApplicationConverter {
|
||||||
const rwx = KubernetesApplicationHelper.hasRWX(claims);
|
const rwx = KubernetesApplicationHelper.hasRWX(claims);
|
||||||
|
|
||||||
const deployment =
|
const deployment =
|
||||||
(formValues.DeploymentType === KubernetesApplicationDeploymentTypes.REPLICATED &&
|
(formValues.DeploymentType === KubernetesApplicationDeploymentTypes.Replicated &&
|
||||||
(claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.SHARED))) ||
|
(claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.Shared))) ||
|
||||||
formValues.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT;
|
formValues.ApplicationType === KubernetesApplicationTypes.Deployment;
|
||||||
|
|
||||||
const statefulSet =
|
const statefulSet =
|
||||||
(formValues.DeploymentType === KubernetesApplicationDeploymentTypes.REPLICATED &&
|
(formValues.DeploymentType === KubernetesApplicationDeploymentTypes.Replicated &&
|
||||||
claims.length > 0 &&
|
claims.length > 0 &&
|
||||||
formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.ISOLATED) ||
|
formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.Isolated) ||
|
||||||
formValues.ApplicationType === KubernetesApplicationTypes.STATEFULSET;
|
formValues.ApplicationType === KubernetesApplicationTypes.StatefulSet;
|
||||||
|
|
||||||
const daemonSet =
|
const daemonSet =
|
||||||
(formValues.DeploymentType === KubernetesApplicationDeploymentTypes.GLOBAL &&
|
(formValues.DeploymentType === KubernetesApplicationDeploymentTypes.Global &&
|
||||||
(claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.SHARED && rwx))) ||
|
(claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.Shared && rwx))) ||
|
||||||
formValues.ApplicationType === KubernetesApplicationTypes.DAEMONSET;
|
formValues.ApplicationType === KubernetesApplicationTypes.DaemonSet;
|
||||||
|
|
||||||
let app;
|
let app;
|
||||||
if (deployment) {
|
if (deployment) {
|
||||||
|
@ -363,6 +351,7 @@ class KubernetesApplicationConverter {
|
||||||
} else {
|
} else {
|
||||||
throw new PortainerError('Unable to determine which association to use to convert form');
|
throw new PortainerError('Unable to determine which association to use to convert form');
|
||||||
}
|
}
|
||||||
|
app.ApplicationType = formValues.ApplicationType;
|
||||||
|
|
||||||
let headlessService;
|
let headlessService;
|
||||||
if (statefulSet) {
|
if (statefulSet) {
|
||||||
|
|
|
@ -71,7 +71,7 @@ class KubernetesPersistentVolumeClaimConverter {
|
||||||
res.metadata.namespace = pvc.Namespace;
|
res.metadata.namespace = pvc.Namespace;
|
||||||
res.spec.resources.requests.storage = pvc.Storage;
|
res.spec.resources.requests.storage = pvc.Storage;
|
||||||
res.spec.storageClassName = pvc.storageClass ? pvc.storageClass.Name : '';
|
res.spec.storageClassName = pvc.storageClass ? pvc.storageClass.Name : '';
|
||||||
const accessModes = pvc.StorageClass && pvc.StorageClass.AccessModes ? pvc.StorageClass.AccessModes.map((accessMode) => storageClassToPVCAccessModes[accessMode]) : [];
|
const accessModes = pvc.storageClass && pvc.storageClass.AccessModes ? pvc.storageClass.AccessModes.map((accessMode) => storageClassToPVCAccessModes[accessMode]) : [];
|
||||||
res.spec.accessModes = accessModes;
|
res.spec.accessModes = accessModes;
|
||||||
res.metadata.labels.app = pvc.ApplicationName;
|
res.metadata.labels.app = pvc.ApplicationName;
|
||||||
res.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = pvc.ApplicationOwner;
|
res.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = pvc.ApplicationOwner;
|
||||||
|
|
|
@ -3,7 +3,6 @@ import * as JsonPatch from 'fast-json-patch';
|
||||||
|
|
||||||
import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads';
|
import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads';
|
||||||
import {
|
import {
|
||||||
KubernetesApplicationPublishingTypes,
|
|
||||||
KubernetesPortainerApplicationNameLabel,
|
KubernetesPortainerApplicationNameLabel,
|
||||||
KubernetesPortainerApplicationOwnerLabel,
|
KubernetesPortainerApplicationOwnerLabel,
|
||||||
KubernetesPortainerApplicationStackNameLabel,
|
KubernetesPortainerApplicationStackNameLabel,
|
||||||
|
@ -42,11 +41,7 @@ class KubernetesServiceConverter {
|
||||||
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
|
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
|
||||||
res.ApplicationOwner = formValues.ApplicationOwner;
|
res.ApplicationOwner = formValues.ApplicationOwner;
|
||||||
res.ApplicationName = formValues.Name;
|
res.ApplicationName = formValues.Name;
|
||||||
if (formValues.PublishingType === KubernetesApplicationPublishingTypes.NODE_PORT) {
|
res.Type = formValues.PublishingType;
|
||||||
res.Type = KubernetesServiceTypes.NODE_PORT;
|
|
||||||
} else if (formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
|
|
||||||
res.Type = KubernetesServiceTypes.LOAD_BALANCER;
|
|
||||||
}
|
|
||||||
const ports = _.map(formValues.PublishedPorts, (item) => _publishedPortToServicePort(formValues, item, res.Type));
|
const ports = _.map(formValues.PublishedPorts, (item) => _publishedPortToServicePort(formValues, item, res.Type));
|
||||||
res.Ports = _.uniqBy(_.without(ports, undefined), (p) => p.targetPort + p.protocol);
|
res.Ports = _.uniqBy(_.without(ports, undefined), (p) => p.targetPort + p.protocol);
|
||||||
return res;
|
return res;
|
||||||
|
@ -61,13 +56,7 @@ class KubernetesServiceConverter {
|
||||||
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
|
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
|
||||||
res.ApplicationOwner = formValues.ApplicationOwner;
|
res.ApplicationOwner = formValues.ApplicationOwner;
|
||||||
res.ApplicationName = formValues.Name;
|
res.ApplicationName = formValues.Name;
|
||||||
if (service.Type === KubernetesApplicationPublishingTypes.NODE_PORT) {
|
res.Type = service.Type;
|
||||||
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;
|
res.Ingress = service.Ingress;
|
||||||
|
|
||||||
if (service.Selector !== undefined) {
|
if (service.Selector !== undefined) {
|
||||||
|
@ -120,18 +109,7 @@ class KubernetesServiceConverter {
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName;
|
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName;
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner;
|
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner;
|
||||||
|
|
||||||
const ports = [];
|
payload.spec.ports = service.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;
|
payload.spec.selector = service.Selector;
|
||||||
if (service.Headless) {
|
if (service.Headless) {
|
||||||
payload.spec.clusterIP = KubernetesServiceHeadlessClusterIP;
|
payload.spec.clusterIP = KubernetesServiceHeadlessClusterIP;
|
||||||
|
|
|
@ -1,29 +1,8 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import { KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models';
|
|
||||||
import { KubernetesApplicationTypes, KubernetesApplicationTypeStrings } from 'Kubernetes/models/application/models';
|
|
||||||
import { nodeAffinityValues } from './application';
|
import { nodeAffinityValues } from './application';
|
||||||
|
|
||||||
angular
|
angular
|
||||||
.module('portainer.kubernetes')
|
.module('portainer.kubernetes')
|
||||||
.filter('kubernetesApplicationTypeText', function () {
|
|
||||||
'use strict';
|
|
||||||
return function (type) {
|
|
||||||
switch (type) {
|
|
||||||
case KubernetesApplicationTypes.DEPLOYMENT:
|
|
||||||
return KubernetesApplicationTypeStrings.DEPLOYMENT;
|
|
||||||
case KubernetesApplicationTypes.DAEMONSET:
|
|
||||||
return KubernetesApplicationTypeStrings.DAEMONSET;
|
|
||||||
case KubernetesApplicationTypes.STATEFULSET:
|
|
||||||
return KubernetesApplicationTypeStrings.STATEFULSET;
|
|
||||||
case KubernetesApplicationTypes.POD:
|
|
||||||
return KubernetesApplicationTypeStrings.POD;
|
|
||||||
case KubernetesApplicationTypes.HELM:
|
|
||||||
return KubernetesApplicationTypeStrings.HELM;
|
|
||||||
default:
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter('kubernetesApplicationCPUValue', function () {
|
.filter('kubernetesApplicationCPUValue', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (value) {
|
return function (value) {
|
||||||
|
@ -34,31 +13,20 @@ angular
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (value) {
|
return function (value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case KubernetesApplicationDataAccessPolicies.ISOLATED:
|
case 'Isolated':
|
||||||
return 'boxes';
|
return 'boxes';
|
||||||
case KubernetesApplicationDataAccessPolicies.SHARED:
|
case 'Shared':
|
||||||
return 'box';
|
return 'box';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter('kubernetesApplicationDataAccessPolicyText', function () {
|
|
||||||
'use strict';
|
|
||||||
return function (value) {
|
|
||||||
switch (value) {
|
|
||||||
case KubernetesApplicationDataAccessPolicies.ISOLATED:
|
|
||||||
return 'Isolated';
|
|
||||||
case KubernetesApplicationDataAccessPolicies.SHARED:
|
|
||||||
return 'Shared';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter('kubernetesApplicationDataAccessPolicyTooltip', function () {
|
.filter('kubernetesApplicationDataAccessPolicyTooltip', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (value) {
|
return function (value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case KubernetesApplicationDataAccessPolicies.ISOLATED:
|
case 'Isolated':
|
||||||
return 'All the instances of this application are using their own data.';
|
return 'All the instances of this application are using their own data.';
|
||||||
case KubernetesApplicationDataAccessPolicies.SHARED:
|
case 'Shared':
|
||||||
return 'All the instances of this application are sharing the same data.';
|
return 'All the instances of this application are sharing the same data.';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,7 +22,8 @@ import {
|
||||||
KubernetesApplicationVolumeSecretPayload,
|
KubernetesApplicationVolumeSecretPayload,
|
||||||
} from 'Kubernetes/models/application/payloads';
|
} from 'Kubernetes/models/application/payloads';
|
||||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||||
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes, HelmApplication } from 'Kubernetes/models/application/models';
|
import { HelmApplication } from 'Kubernetes/models/application/models';
|
||||||
|
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
|
||||||
import { KubernetesPodAffinity, KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
|
import { KubernetesPodAffinity, KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
|
||||||
import {
|
import {
|
||||||
KubernetesNodeSelectorRequirementPayload,
|
KubernetesNodeSelectorRequirementPayload,
|
||||||
|
@ -287,13 +288,6 @@ class KubernetesApplicationHelper {
|
||||||
svc.ApplicationOwner = app.ApplicationOwner;
|
svc.ApplicationOwner = app.ApplicationOwner;
|
||||||
svc.ApplicationName = app.ApplicationName;
|
svc.ApplicationName = app.ApplicationName;
|
||||||
svc.Type = service.spec.type;
|
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 = [];
|
let ports = [];
|
||||||
service.spec.ports.forEach(function (port) {
|
service.spec.ports.forEach(function (port) {
|
||||||
|
@ -373,15 +367,15 @@ class KubernetesApplicationHelper {
|
||||||
static generateAutoScalerFormValueFromHorizontalPodAutoScaler(autoScaler, replicasCount) {
|
static generateAutoScalerFormValueFromHorizontalPodAutoScaler(autoScaler, replicasCount) {
|
||||||
const res = new KubernetesApplicationAutoScalerFormValue();
|
const res = new KubernetesApplicationAutoScalerFormValue();
|
||||||
if (autoScaler) {
|
if (autoScaler) {
|
||||||
res.IsUsed = true;
|
res.isUsed = true;
|
||||||
res.MinReplicas = autoScaler.MinReplicas;
|
res.minReplicas = autoScaler.MinReplicas;
|
||||||
res.MaxReplicas = autoScaler.MaxReplicas;
|
res.maxReplicas = autoScaler.MaxReplicas;
|
||||||
res.TargetCPUUtilization = autoScaler.TargetCPUUtilization;
|
res.targetCpuUtilizationPercentage = autoScaler.TargetCPUUtilization;
|
||||||
res.ApiVersion = autoScaler.ApiVersion;
|
res.apiVersion = autoScaler.ApiVersion;
|
||||||
} else {
|
} else {
|
||||||
res.ApiVersion = 'apps/v1';
|
res.apiVersion = 'apps/v1';
|
||||||
res.MinReplicas = replicasCount;
|
res.minReplicas = replicasCount;
|
||||||
res.MaxReplicas = replicasCount;
|
res.maxReplicas = replicasCount;
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
@ -461,7 +455,7 @@ class KubernetesApplicationHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
static generateAffinityFromPlacements(app, formValues) {
|
static generateAffinityFromPlacements(app, formValues) {
|
||||||
if (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.REPLICATED) {
|
if (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.Replicated) {
|
||||||
const placements = formValues.Placements;
|
const placements = formValues.Placements;
|
||||||
const res = new KubernetesPodNodeAffinityPayload();
|
const res = new KubernetesPodNodeAffinityPayload();
|
||||||
let expressions = _.map(placements, (p) => {
|
let expressions = _.map(placements, (p) => {
|
||||||
|
@ -545,7 +539,7 @@ class KubernetesApplicationHelper {
|
||||||
const helmAppsList = helmAppsEntriesList.map(([helmInstance, applications]) => {
|
const helmAppsList = helmAppsEntriesList.map(([helmInstance, applications]) => {
|
||||||
const helmApp = new HelmApplication();
|
const helmApp = new HelmApplication();
|
||||||
helmApp.Name = helmInstance;
|
helmApp.Name = helmInstance;
|
||||||
helmApp.ApplicationType = KubernetesApplicationTypes.HELM;
|
helmApp.ApplicationType = KubernetesApplicationTypes.Helm;
|
||||||
helmApp.ApplicationOwner = applications[0].ApplicationOwner;
|
helmApp.ApplicationOwner = applications[0].ApplicationOwner;
|
||||||
helmApp.KubernetesApplications = applications;
|
helmApp.KubernetesApplications = applications;
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import uuidv4 from 'uuid/v4';
|
import uuidv4 from 'uuid/v4';
|
||||||
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
|
||||||
|
|
||||||
class KubernetesVolumeHelper {
|
class KubernetesVolumeHelper {
|
||||||
// TODO: review
|
// TODO: review
|
||||||
// the following condition
|
// the following condition
|
||||||
// && (app.ApplicationType === KubernetesApplicationTypes.STATEFULSET ? _.includes(volume.PersistentVolumeClaim.Name, app.Name) : true);
|
// && (app.ApplicationType === KubernetesApplicationTypes.StatefulSet ? _.includes(volume.PersistentVolumeClaim.Name, app.Name) : true);
|
||||||
// is made to enforce finding the good SFS when multiple SFS in the same namespace
|
// is made to enforce finding the good SFS when multiple SFS in the same namespace
|
||||||
// are referencing an internal PVC using the same internal name
|
// are referencing an internal PVC using the same internal name
|
||||||
// (PVC are not exposed to other apps so they can have the same name in differents SFS)
|
// (PVC are not exposed to other apps so they can have the same name in differents SFS)
|
||||||
|
@ -16,7 +16,7 @@ class KubernetesVolumeHelper {
|
||||||
return (
|
return (
|
||||||
volume.ResourcePool.Namespace.Name === app.ResourcePool &&
|
volume.ResourcePool.Namespace.Name === app.ResourcePool &&
|
||||||
matchingNames.length &&
|
matchingNames.length &&
|
||||||
(app.ApplicationType === KubernetesApplicationTypes.STATEFULSET ? _.includes(volume.PersistentVolumeClaim.Name, app.Name) : true)
|
(app.ApplicationType === KubernetesApplicationTypes.StatefulSet ? _.includes(volume.PersistentVolumeClaim.Name, app.Name) : true)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ export class KubernetesHorizontalPodAutoScalerConverter {
|
||||||
payload.metadata.name = data.TargetEntity.Name;
|
payload.metadata.name = data.TargetEntity.Name;
|
||||||
payload.spec.minReplicas = data.MinReplicas;
|
payload.spec.minReplicas = data.MinReplicas;
|
||||||
payload.spec.maxReplicas = data.MaxReplicas;
|
payload.spec.maxReplicas = data.MaxReplicas;
|
||||||
payload.spec.targetCPUUtilizationPercentage = data.TargetCPUUtilization;
|
payload.spec.targetCPUUtilizationPercentage = data.targetCpuUtilizationPercentage;
|
||||||
payload.spec.scaleTargetRef.apiVersion = data.TargetEntity.ApiVersion;
|
payload.spec.scaleTargetRef.apiVersion = data.TargetEntity.ApiVersion;
|
||||||
payload.spec.scaleTargetRef.kind = data.TargetEntity.Kind;
|
payload.spec.scaleTargetRef.kind = data.TargetEntity.Kind;
|
||||||
payload.spec.scaleTargetRef.name = data.TargetEntity.Name;
|
payload.spec.scaleTargetRef.name = data.TargetEntity.Name;
|
||||||
|
@ -48,86 +48,12 @@ export class KubernetesHorizontalPodAutoScalerConverter {
|
||||||
const res = new KubernetesHorizontalPodAutoScaler();
|
const res = new KubernetesHorizontalPodAutoScaler();
|
||||||
res.Name = formValues.Name;
|
res.Name = formValues.Name;
|
||||||
res.Namespace = formValues.ResourcePool.Namespace.Name;
|
res.Namespace = formValues.ResourcePool.Namespace.Name;
|
||||||
res.MinReplicas = formValues.AutoScaler.MinReplicas;
|
res.MinReplicas = formValues.AutoScaler.minReplicas;
|
||||||
res.MaxReplicas = formValues.AutoScaler.MaxReplicas;
|
res.MaxReplicas = formValues.AutoScaler.maxReplicas;
|
||||||
res.TargetCPUUtilization = formValues.AutoScaler.TargetCPUUtilization;
|
res.TargetCPUUtilization = formValues.AutoScaler.targetCpuUtilizationPercentage;
|
||||||
res.TargetEntity.Name = formValues.Name;
|
res.TargetEntity.Name = formValues.Name;
|
||||||
res.TargetEntity.Kind = kind;
|
res.TargetEntity.Kind = kind;
|
||||||
res.TargetEntity.ApiVersion = formValues.AutoScaler.ApiVersion;
|
res.TargetEntity.ApiVersion = formValues.AutoScaler.apiVersion;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convertion functions to use with v2beta2 model
|
|
||||||
*/
|
|
||||||
|
|
||||||
// static apiToModel(data, yaml) {
|
|
||||||
// const res = new KubernetesHorizontalPodAutoScaler();
|
|
||||||
// res.Id = data.metadata.uid;
|
|
||||||
// res.Namespace = data.metadata.namespace;
|
|
||||||
// res.Name = data.metadata.name;
|
|
||||||
// res.MinReplicas = data.spec.minReplicas;
|
|
||||||
// res.MaxReplicas = data.spec.maxReplicas;
|
|
||||||
// res.TargetCPUUtilization = data.spec.targetCPUUtilization;
|
|
||||||
|
|
||||||
// _.forEach(data.spec.metrics, (metric) => {
|
|
||||||
// if (metric.type === 'Resource') {
|
|
||||||
// if (metric.resource.name === 'cpu') {
|
|
||||||
// res.TargetCPUUtilization = metric.resource.target.averageUtilization;
|
|
||||||
// }
|
|
||||||
// if (metric.resource.name === 'memory') {
|
|
||||||
// res.TargetMemoryValue = parseFloat(metric.resource.target.averageValue) / 1000;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (data.spec.scaleTargetRef) {
|
|
||||||
// res.TargetEntity.ApiVersion = data.spec.scaleTargetRef.apiVersion;
|
|
||||||
// res.TargetEntity.Kind = data.spec.scaleTargetRef.kind;
|
|
||||||
// res.TargetEntity.Name = data.spec.scaleTargetRef.name;
|
|
||||||
// }
|
|
||||||
// res.Yaml = yaml ? yaml.data : '';
|
|
||||||
// return res;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// static createPayload(data) {
|
|
||||||
// const payload = new KubernetesHorizontalPodAutoScalerCreatePayload();
|
|
||||||
// payload.metadata.namespace = data.Namespace;
|
|
||||||
// payload.metadata.name = data.TargetEntity.Name;
|
|
||||||
// payload.spec.minReplicas = data.MinReplicas;
|
|
||||||
// payload.spec.maxReplicas = data.MaxReplicas;
|
|
||||||
|
|
||||||
// if (data.TargetMemoryValue) {
|
|
||||||
// const memoryMetric = new KubernetesHorizontalPodAutoScalerMemoryMetric();
|
|
||||||
// memoryMetric.resource.target.averageValue = data.TargetMemoryValue;
|
|
||||||
// payload.spec.metrics.push(memoryMetric);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (data.TargetCPUUtilization) {
|
|
||||||
// const cpuMetric = new KubernetesHorizontalPodAutoScalerCPUMetric();
|
|
||||||
// cpuMetric.resource.target.averageUtilization = data.TargetCPUUtilization;
|
|
||||||
// payload.spec.metrics.push(cpuMetric);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// payload.spec.scaleTargetRef.apiVersion = data.TargetEntity.ApiVersion;
|
|
||||||
// payload.spec.scaleTargetRef.kind = data.TargetEntity.Kind;
|
|
||||||
// payload.spec.scaleTargetRef.name = data.TargetEntity.Name;
|
|
||||||
|
|
||||||
// return payload;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// static applicationFormValuesToModel(formValues, kind) {
|
|
||||||
// const res = new KubernetesHorizontalPodAutoScaler();
|
|
||||||
// res.Name = formValues.Name;
|
|
||||||
// res.Namespace = formValues.ResourcePool.Namespace.Name;
|
|
||||||
// res.MinReplicas = formValues.AutoScaler.MinReplicas;
|
|
||||||
// res.MaxReplicas = formValues.AutoScaler.MaxReplicas;
|
|
||||||
// res.TargetCPUUtilization = formValues.AutoScaler.TargetCPUUtilization;
|
|
||||||
// if (formValues.AutoScaler.TargetMemoryValue) {
|
|
||||||
// res.TargetMemoryValue = formValues.AutoScaler.TargetMemoryValue + 'M';
|
|
||||||
// }
|
|
||||||
// res.TargetEntity.Name = formValues.Name;
|
|
||||||
// res.TargetEntity.Kind = kind;
|
|
||||||
// return res;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,8 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import PortainerError from 'Portainer/error';
|
|
||||||
import { KubernetesApplication, KubernetesApplicationTypes, KubernetesApplicationTypeStrings } from 'Kubernetes/models/application/models';
|
|
||||||
import { KubernetesDeployment } from 'Kubernetes/models/deployment/models';
|
|
||||||
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
|
|
||||||
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
|
|
||||||
|
|
||||||
export class KubernetesHorizontalPodAutoScalerHelper {
|
export class KubernetesHorizontalPodAutoScalerHelper {
|
||||||
static findApplicationBoundScaler(sList, app) {
|
static findApplicationBoundScaler(sList, app) {
|
||||||
const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app);
|
const kind = app.ApplicationType;
|
||||||
return _.find(sList, (item) => item.TargetEntity.Kind === kind && item.TargetEntity.Name === app.Name);
|
return _.find(sList, (item) => item.TargetEntity.Kind === kind && item.TargetEntity.Name === app.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getApplicationTypeString(app) {
|
|
||||||
if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT) || app instanceof KubernetesDeployment) {
|
|
||||||
return KubernetesApplicationTypeStrings.DEPLOYMENT;
|
|
||||||
} else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET) || app instanceof KubernetesDaemonSet) {
|
|
||||||
return KubernetesApplicationTypeStrings.DAEMONSET;
|
|
||||||
} else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET) || app instanceof KubernetesStatefulSet) {
|
|
||||||
return KubernetesApplicationTypeStrings.STATEFULSET;
|
|
||||||
} else if (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.POD) {
|
|
||||||
return KubernetesApplicationTypeStrings.POD;
|
|
||||||
} else {
|
|
||||||
throw new PortainerError('Unable to determine application type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
KubernetesResourcePoolIngressClassFormValue,
|
KubernetesResourcePoolIngressClassFormValue,
|
||||||
KubernetesResourcePoolIngressClassHostFormValue,
|
KubernetesResourcePoolIngressClassHostFormValue,
|
||||||
} from 'Kubernetes/models/resource-pool/formValues';
|
} from 'Kubernetes/models/resource-pool/formValues';
|
||||||
import { KubernetesApplicationPublishingTypes } from '../models/application/models';
|
|
||||||
import { KubernetesIngress, KubernetesIngressRule } from './models';
|
import { KubernetesIngress, KubernetesIngressRule } from './models';
|
||||||
import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads';
|
import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads';
|
||||||
import { KubernetesIngressClassAnnotation, PortainerIngressClassTypes } from './constants';
|
import { KubernetesIngressClassAnnotation, PortainerIngressClassTypes } from './constants';
|
||||||
|
@ -48,36 +47,6 @@ export class KubernetesIngressConverter {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts Application Form Value (from Create Application View) to Ingresses
|
|
||||||
* @param {KubernetesApplicationFormValues} formValues
|
|
||||||
* @param {string} serviceName
|
|
||||||
* @returns {KubernetesIngressRule[]}
|
|
||||||
*/
|
|
||||||
static applicationFormValuesToIngresses(formValues, serviceName) {
|
|
||||||
const isPublishingToIngress = formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS;
|
|
||||||
const ingresses = angular.copy(formValues.OriginalIngresses);
|
|
||||||
_.forEach(formValues.PublishedPorts, (p) => {
|
|
||||||
const ingress = _.find(ingresses, { Name: p.IngressName });
|
|
||||||
if (ingress) {
|
|
||||||
if (p.NeedsDeletion) {
|
|
||||||
_.remove(ingress.Paths, (path) => path.Port === p.ContainerPort && path.ServiceName === serviceName && path.Path === p.IngressRoute);
|
|
||||||
} else if (isPublishingToIngress && p.IsNew) {
|
|
||||||
const rule = new KubernetesIngressRule();
|
|
||||||
rule.IngressName = ingress.Name;
|
|
||||||
rule.ServiceName = serviceName;
|
|
||||||
rule.Port = p.ContainerPort;
|
|
||||||
if (p.IngressRoute) {
|
|
||||||
rule.Path = _.startsWith(p.IngressRoute, '/') ? p.IngressRoute : '/' + p.IngressRoute;
|
|
||||||
}
|
|
||||||
rule.Host = p.IngressHost;
|
|
||||||
ingress.Paths.push(rule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return ingresses;
|
|
||||||
}
|
|
||||||
|
|
||||||
static applicationFormValuesToDeleteIngresses(formValues, application) {
|
static applicationFormValuesToDeleteIngresses(formValues, application) {
|
||||||
const ingresses = angular.copy(formValues.OriginalIngresses);
|
const ingresses = angular.copy(formValues.OriginalIngresses);
|
||||||
application.Services.forEach((service) => {
|
application.Services.forEach((service) => {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { PorImageRegistryModel } from '@/docker/models/porImageRegistry';
|
import { PorImageRegistryModel } from '@/docker/models/porImageRegistry';
|
||||||
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes } from './models';
|
import { KubernetesApplicationTypes, KubernetesApplicationDeploymentTypes, KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models/appConstants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KubernetesApplicationFormValues Model
|
* KubernetesApplicationFormValues Model
|
||||||
*/
|
*/
|
||||||
export function KubernetesApplicationFormValues() {
|
export function KubernetesApplicationFormValues() {
|
||||||
this.ApplicationType = undefined; // will only exist for formValues generated from Application (app edit situation;
|
this.ApplicationType = KubernetesApplicationTypes.Deployment; // will only exist for formValues generated from Application (app edit situation;
|
||||||
this.ResourcePool = {};
|
this.ResourcePool = {};
|
||||||
this.Name = '';
|
this.Name = '';
|
||||||
this.StackName = '';
|
this.StackName = '';
|
||||||
|
@ -14,13 +14,13 @@ export function KubernetesApplicationFormValues() {
|
||||||
this.Note = '';
|
this.Note = '';
|
||||||
this.MemoryLimit = 0;
|
this.MemoryLimit = 0;
|
||||||
this.CpuLimit = 0;
|
this.CpuLimit = 0;
|
||||||
this.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
|
this.DeploymentType = KubernetesApplicationDeploymentTypes.Replicated;
|
||||||
this.ReplicaCount = 1;
|
this.ReplicaCount = 1;
|
||||||
this.AutoScaler = {};
|
this.AutoScaler = {};
|
||||||
this.Containers = [];
|
this.Containers = [];
|
||||||
this.Services = [];
|
this.Services = [];
|
||||||
this.EnvironmentVariables = []; // KubernetesApplicationEnvironmentVariableFormValue lis;
|
this.EnvironmentVariables = []; // KubernetesApplicationEnvironmentVariableFormValue lis;
|
||||||
this.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;
|
this.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.Isolated;
|
||||||
this.PersistedFolders = []; // KubernetesApplicationPersistedFolderFormValue lis;
|
this.PersistedFolders = []; // KubernetesApplicationPersistedFolderFormValue lis;
|
||||||
this.ConfigMaps = [];
|
this.ConfigMaps = [];
|
||||||
this.Secrets = [];
|
this.Secrets = [];
|
||||||
|
@ -130,11 +130,11 @@ export function KubernetesApplicationPlacementFormValue() {
|
||||||
* KubernetesApplicationAutoScalerFormValue Model
|
* KubernetesApplicationAutoScalerFormValue Model
|
||||||
*/
|
*/
|
||||||
const _KubernetesApplicationAutoScalerFormValue = Object.freeze({
|
const _KubernetesApplicationAutoScalerFormValue = Object.freeze({
|
||||||
MinReplicas: 0,
|
minReplicas: 0,
|
||||||
MaxReplicas: 0,
|
maxReplicas: 0,
|
||||||
TargetCPUUtilization: 50,
|
targetCpuUtilizationPercentage: 50,
|
||||||
ApiVersion: '',
|
apiVersion: '',
|
||||||
IsUsed: false,
|
isUsed: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesApplicationAutoScalerFormValue {
|
export class KubernetesApplicationAutoScalerFormValue {
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import {
|
||||||
|
AppType,
|
||||||
|
AppDataAccessPolicy,
|
||||||
|
DeploymentType,
|
||||||
|
} from '@/react/kubernetes/applications/types';
|
||||||
|
import { ServiceType } from '@/react/kubernetes/services/types';
|
||||||
|
|
||||||
|
// The following constants are used by angular views and can be removed once they are no longer referenced
|
||||||
|
export const KubernetesApplicationTypes: Record<AppType, AppType> = {
|
||||||
|
Deployment: 'Deployment',
|
||||||
|
StatefulSet: 'StatefulSet',
|
||||||
|
DaemonSet: 'DaemonSet',
|
||||||
|
Pod: 'Pod',
|
||||||
|
Helm: 'Helm',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const KubernetesApplicationDeploymentTypes: Record<
|
||||||
|
DeploymentType,
|
||||||
|
DeploymentType
|
||||||
|
> = {
|
||||||
|
Global: 'Global',
|
||||||
|
Replicated: 'Replicated',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const KubernetesApplicationDataAccessPolicies: Record<
|
||||||
|
AppDataAccessPolicy,
|
||||||
|
AppDataAccessPolicy
|
||||||
|
> = {
|
||||||
|
Isolated: 'Isolated',
|
||||||
|
Shared: 'Shared',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const KubernetesApplicationServiceTypes: Record<
|
||||||
|
ServiceType,
|
||||||
|
ServiceType
|
||||||
|
> = {
|
||||||
|
ClusterIP: 'ClusterIP',
|
||||||
|
NodePort: 'NodePort',
|
||||||
|
LoadBalancer: 'LoadBalancer',
|
||||||
|
ExternalName: 'ExternalName',
|
||||||
|
} as const;
|
|
@ -1,35 +1,3 @@
|
||||||
export const KubernetesApplicationDeploymentTypes = Object.freeze({
|
|
||||||
REPLICATED: 1,
|
|
||||||
GLOBAL: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const KubernetesApplicationDataAccessPolicies = Object.freeze({
|
|
||||||
SHARED: 1,
|
|
||||||
ISOLATED: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const KubernetesApplicationTypes = Object.freeze({
|
|
||||||
DEPLOYMENT: 1,
|
|
||||||
DAEMONSET: 2,
|
|
||||||
STATEFULSET: 3,
|
|
||||||
POD: 4,
|
|
||||||
HELM: 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const KubernetesApplicationTypeStrings = Object.freeze({
|
|
||||||
HELM: 'Helm',
|
|
||||||
DEPLOYMENT: 'Deployment',
|
|
||||||
DAEMONSET: 'DaemonSet',
|
|
||||||
STATEFULSET: 'StatefulSet',
|
|
||||||
POD: 'Pod',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const KubernetesApplicationPublishingTypes = Object.freeze({
|
|
||||||
CLUSTER_IP: 1,
|
|
||||||
NODE_PORT: 2,
|
|
||||||
LOAD_BALANCER: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const KubernetesApplicationQuotaDefaults = {
|
export const KubernetesApplicationQuotaDefaults = {
|
||||||
CpuLimit: 0.1,
|
CpuLimit: 0.1,
|
||||||
MemoryLimit: 64, // MB
|
MemoryLimit: 64, // MB
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { NamespacesSelector } from '@/react/kubernetes/cluster/RegistryAccessVie
|
||||||
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/ConfigureForm/StorageAccessModeSelector';
|
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/ConfigureForm/StorageAccessModeSelector';
|
||||||
import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/AccessView/NamespaceAccessUsersSelector';
|
import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/AccessView/NamespaceAccessUsersSelector';
|
||||||
import { RegistriesSelector } from '@/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesSelector';
|
import { RegistriesSelector } from '@/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesSelector';
|
||||||
import { DataAccessPolicyFormSection } from '@/react/kubernetes/applications/CreateView/DataAccessPolicyFormSection';
|
|
||||||
import { KubeServicesForm } from '@/react/kubernetes/applications/CreateView/application-services/KubeServicesForm';
|
import { KubeServicesForm } from '@/react/kubernetes/applications/CreateView/application-services/KubeServicesForm';
|
||||||
import { kubeServicesValidation } from '@/react/kubernetes/applications/CreateView/application-services/kubeServicesValidation';
|
import { kubeServicesValidation } from '@/react/kubernetes/applications/CreateView/application-services/kubeServicesValidation';
|
||||||
import { AppDeploymentTypeFormSection } from '@/react/kubernetes/applications/CreateView/AppDeploymentTypeFormSection';
|
import { AppDeploymentTypeFormSection } from '@/react/kubernetes/applications/CreateView/AppDeploymentTypeFormSection';
|
||||||
|
@ -22,6 +21,7 @@ import {
|
||||||
PlacementFormSection,
|
PlacementFormSection,
|
||||||
placementValidation,
|
placementValidation,
|
||||||
} from '@/react/kubernetes/applications/components/PlacementFormSection';
|
} from '@/react/kubernetes/applications/components/PlacementFormSection';
|
||||||
|
import { ApplicationSummarySection } from '@/react/kubernetes/applications/components/ApplicationSummarySection';
|
||||||
import { withFormValidation } from '@/react-tools/withFormValidation';
|
import { withFormValidation } from '@/react-tools/withFormValidation';
|
||||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
import { YAMLInspector } from '@/react/kubernetes/components/YAMLInspector';
|
import { YAMLInspector } from '@/react/kubernetes/components/YAMLInspector';
|
||||||
|
@ -33,6 +33,7 @@ import { SecretsFormSection } from '@/react/kubernetes/applications/components/C
|
||||||
import { configurationsValidationSchema } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/configurationValidationSchema';
|
import { configurationsValidationSchema } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/configurationValidationSchema';
|
||||||
import { ConfigMapsFormSection } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/ConfigMapsFormSection';
|
import { ConfigMapsFormSection } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/ConfigMapsFormSection';
|
||||||
import { PersistedFoldersFormSection } from '@/react/kubernetes/applications/components/PersistedFoldersFormSection';
|
import { PersistedFoldersFormSection } from '@/react/kubernetes/applications/components/PersistedFoldersFormSection';
|
||||||
|
import { DataAccessPolicyFormSection } from '@/react/kubernetes/applications/CreateView/DataAccessPolicyFormSection';
|
||||||
import { persistedFoldersValidation } from '@/react/kubernetes/applications/components/PersistedFoldersFormSection/persistedFoldersValidation';
|
import { persistedFoldersValidation } from '@/react/kubernetes/applications/components/PersistedFoldersFormSection/persistedFoldersValidation';
|
||||||
import {
|
import {
|
||||||
ResourceReservationFormSection,
|
ResourceReservationFormSection,
|
||||||
|
@ -46,6 +47,7 @@ import {
|
||||||
AutoScalingFormSection,
|
AutoScalingFormSection,
|
||||||
autoScalingValidation,
|
autoScalingValidation,
|
||||||
} from '@/react/kubernetes/applications/components/AutoScalingFormSection';
|
} from '@/react/kubernetes/applications/components/AutoScalingFormSection';
|
||||||
|
import { withControlledInput } from '@/react-tools/withControlledInput';
|
||||||
|
|
||||||
import { EnvironmentVariablesFieldset } from '@@/form-components/EnvironmentVariablesFieldset';
|
import { EnvironmentVariablesFieldset } from '@@/form-components/EnvironmentVariablesFieldset';
|
||||||
|
|
||||||
|
@ -112,7 +114,7 @@ export const ngModule = angular
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(NodesDatatable))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(NodesDatatable))), [])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
'dataAccessPolicyFormSection',
|
'accessPolicyFormSection',
|
||||||
r2a(DataAccessPolicyFormSection, [
|
r2a(DataAccessPolicyFormSection, [
|
||||||
'value',
|
'value',
|
||||||
'onChange',
|
'onChange',
|
||||||
|
@ -174,6 +176,13 @@ export const ngModule = angular
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.component(
|
||||||
|
'applicationSummarySection',
|
||||||
|
r2a(
|
||||||
|
withUIRouter(withReactQuery(withCurrentUser(ApplicationSummarySection))),
|
||||||
|
['formValues', 'oldFormValues']
|
||||||
|
)
|
||||||
|
)
|
||||||
.component(
|
.component(
|
||||||
'kubernetesApplicationsStacksDatatable',
|
'kubernetesApplicationsStacksDatatable',
|
||||||
r2a(withUIRouter(withCurrentUser(ApplicationsStacksDatatable)), [
|
r2a(withUIRouter(withCurrentUser(ApplicationsStacksDatatable)), [
|
||||||
|
@ -193,7 +202,9 @@ export const componentsModule = ngModule.name;
|
||||||
|
|
||||||
withFormValidation(
|
withFormValidation(
|
||||||
ngModule,
|
ngModule,
|
||||||
withUIRouter(withCurrentUser(withReactQuery(KubeServicesForm))),
|
withControlledInput(
|
||||||
|
withUIRouter(withCurrentUser(withReactQuery(KubeServicesForm)))
|
||||||
|
),
|
||||||
'kubeServicesForm',
|
'kubeServicesForm',
|
||||||
['values', 'onChange', 'appName', 'selector', 'isEditMode', 'namespace'],
|
['values', 'onChange', 'appName', 'selector', 'isEditMode', 'namespace'],
|
||||||
kubeServicesValidation
|
kubeServicesValidation
|
||||||
|
@ -201,7 +212,7 @@ withFormValidation(
|
||||||
|
|
||||||
withFormValidation(
|
withFormValidation(
|
||||||
ngModule,
|
ngModule,
|
||||||
EnvironmentVariablesFieldset,
|
withControlledInput(EnvironmentVariablesFieldset),
|
||||||
'kubeEnvironmentVariablesFieldset',
|
'kubeEnvironmentVariablesFieldset',
|
||||||
['canUndoDelete'],
|
['canUndoDelete'],
|
||||||
// use kubeEnvVarValidationSchema instead of envVarValidation to add a regex matches rule
|
// use kubeEnvVarValidationSchema instead of envVarValidation to add a regex matches rule
|
||||||
|
@ -210,7 +221,9 @@ withFormValidation(
|
||||||
|
|
||||||
withFormValidation(
|
withFormValidation(
|
||||||
ngModule,
|
ngModule,
|
||||||
withUIRouter(withCurrentUser(withReactQuery(ConfigMapsFormSection))),
|
withControlledInput(
|
||||||
|
withUIRouter(withCurrentUser(withReactQuery(ConfigMapsFormSection)))
|
||||||
|
),
|
||||||
'configMapsFormSection',
|
'configMapsFormSection',
|
||||||
['values', 'onChange', 'namespace'],
|
['values', 'onChange', 'namespace'],
|
||||||
configurationsValidationSchema
|
configurationsValidationSchema
|
||||||
|
@ -218,7 +231,9 @@ withFormValidation(
|
||||||
|
|
||||||
withFormValidation(
|
withFormValidation(
|
||||||
ngModule,
|
ngModule,
|
||||||
withUIRouter(withCurrentUser(withReactQuery(SecretsFormSection))),
|
withControlledInput(
|
||||||
|
withUIRouter(withCurrentUser(withReactQuery(SecretsFormSection)))
|
||||||
|
),
|
||||||
'secretsFormSection',
|
'secretsFormSection',
|
||||||
['values', 'onChange', 'namespace'],
|
['values', 'onChange', 'namespace'],
|
||||||
configurationsValidationSchema
|
configurationsValidationSchema
|
||||||
|
@ -240,7 +255,11 @@ withFormValidation(
|
||||||
|
|
||||||
withFormValidation(
|
withFormValidation(
|
||||||
ngModule,
|
ngModule,
|
||||||
withUIRouter(withCurrentUser(withReactQuery(ResourceReservationFormSection))),
|
withControlledInput(
|
||||||
|
withUIRouter(
|
||||||
|
withCurrentUser(withReactQuery(ResourceReservationFormSection))
|
||||||
|
)
|
||||||
|
),
|
||||||
'resourceReservationFormSection',
|
'resourceReservationFormSection',
|
||||||
[
|
[
|
||||||
'namespaceHasQuota',
|
'namespaceHasQuota',
|
||||||
|
@ -253,7 +272,9 @@ withFormValidation(
|
||||||
|
|
||||||
withFormValidation(
|
withFormValidation(
|
||||||
ngModule,
|
ngModule,
|
||||||
withUIRouter(withCurrentUser(withReactQuery(ReplicationFormSection))),
|
withControlledInput(
|
||||||
|
withUIRouter(withCurrentUser(withReactQuery(ReplicationFormSection)))
|
||||||
|
),
|
||||||
'replicationFormSection',
|
'replicationFormSection',
|
||||||
[
|
[
|
||||||
'supportScalableReplicaDeployment',
|
'supportScalableReplicaDeployment',
|
||||||
|
@ -266,7 +287,9 @@ withFormValidation(
|
||||||
|
|
||||||
withFormValidation(
|
withFormValidation(
|
||||||
ngModule,
|
ngModule,
|
||||||
withUIRouter(withCurrentUser(withReactQuery(AutoScalingFormSection))),
|
withControlledInput(
|
||||||
|
withUIRouter(withCurrentUser(withReactQuery(AutoScalingFormSection)))
|
||||||
|
),
|
||||||
'autoScalingFormSection',
|
'autoScalingFormSection',
|
||||||
['isMetricsEnabled'],
|
['isMetricsEnabled'],
|
||||||
autoScalingValidation
|
autoScalingValidation
|
||||||
|
|
|
@ -2,12 +2,9 @@ import _ from 'lodash-es';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import PortainerError from 'Portainer/error';
|
import PortainerError from 'Portainer/error';
|
||||||
|
|
||||||
import { KubernetesApplication, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
|
||||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||||
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
||||||
import { KubernetesDeployment } from 'Kubernetes/models/deployment/models';
|
|
||||||
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
|
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
|
||||||
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
|
|
||||||
import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper';
|
import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper';
|
||||||
import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper';
|
import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper';
|
||||||
import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter';
|
import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter';
|
||||||
|
@ -15,6 +12,7 @@ import KubernetesPodConverter from 'Kubernetes/pod/converter';
|
||||||
import { notifyError } from '@/portainer/services/notifications';
|
import { notifyError } from '@/portainer/services/notifications';
|
||||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||||
import { generateNewIngressesFromFormPaths } from '@/react/kubernetes/applications/CreateView/application-services/utils';
|
import { generateNewIngressesFromFormPaths } from '@/react/kubernetes/applications/CreateView/application-services/utils';
|
||||||
|
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
|
||||||
|
|
||||||
class KubernetesApplicationService {
|
class KubernetesApplicationService {
|
||||||
/* #region CONSTRUCTOR */
|
/* #region CONSTRUCTOR */
|
||||||
|
@ -58,13 +56,13 @@ class KubernetesApplicationService {
|
||||||
/* #region UTILS */
|
/* #region UTILS */
|
||||||
_getApplicationApiService(app) {
|
_getApplicationApiService(app) {
|
||||||
let apiService;
|
let apiService;
|
||||||
if (app instanceof KubernetesDeployment || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT)) {
|
if (app.ApplicationType === KubernetesApplicationTypes.Deployment) {
|
||||||
apiService = this.KubernetesDeploymentService;
|
apiService = this.KubernetesDeploymentService;
|
||||||
} else if (app instanceof KubernetesDaemonSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET)) {
|
} else if (app.ApplicationType === KubernetesApplicationTypes.DaemonSet) {
|
||||||
apiService = this.KubernetesDaemonSetService;
|
apiService = this.KubernetesDaemonSetService;
|
||||||
} else if (app instanceof KubernetesStatefulSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET)) {
|
} else if (app.ApplicationType === KubernetesApplicationTypes.StatefulSet) {
|
||||||
apiService = this.KubernetesStatefulSetService;
|
apiService = this.KubernetesStatefulSetService;
|
||||||
} else if (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.POD) {
|
} else if (app.ApplicationType === KubernetesApplicationTypes.Pod) {
|
||||||
apiService = this.KubernetesPodService;
|
apiService = this.KubernetesPodService;
|
||||||
} else {
|
} else {
|
||||||
throw new PortainerError('Unable to determine which association to use to retrieve API Service');
|
throw new PortainerError('Unable to determine which association to use to retrieve API Service');
|
||||||
|
@ -257,8 +255,8 @@ class KubernetesApplicationService {
|
||||||
await Promise.all(_.without(claimPromises, undefined));
|
await Promise.all(_.without(claimPromises, undefined));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formValues.AutoScaler.IsUsed && formValues.DeploymentType !== KubernetesApplicationDeploymentTypes.GLOBAL) {
|
if (formValues.AutoScaler.isUsed && formValues.DeploymentType !== KubernetesApplicationDeploymentTypes.Global) {
|
||||||
const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app);
|
const kind = app.ApplicationType;
|
||||||
const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind);
|
const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind);
|
||||||
await this.KubernetesHorizontalPodAutoScalerService.create(autoScaler);
|
await this.KubernetesHorizontalPodAutoScalerService.create(autoScaler);
|
||||||
}
|
}
|
||||||
|
@ -378,16 +376,16 @@ class KubernetesApplicationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp);
|
const newKind = newApp.ApplicationType;
|
||||||
const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind);
|
const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind);
|
||||||
if (!oldFormValues.AutoScaler.IsUsed) {
|
if (!oldFormValues.AutoScaler.isUsed) {
|
||||||
if (newFormValues.AutoScaler.IsUsed) {
|
if (newFormValues.AutoScaler.isUsed) {
|
||||||
await this.KubernetesHorizontalPodAutoScalerService.create(newAutoScaler);
|
await this.KubernetesHorizontalPodAutoScalerService.create(newAutoScaler);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const oldKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(oldApp);
|
const oldKind = oldApp.ApplicationType;
|
||||||
const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind);
|
const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind);
|
||||||
if (newFormValues.AutoScaler.IsUsed) {
|
if (newFormValues.AutoScaler.isUsed) {
|
||||||
await this.KubernetesHorizontalPodAutoScalerService.patch(oldAutoScaler, newAutoScaler);
|
await this.KubernetesHorizontalPodAutoScalerService.patch(oldAutoScaler, newAutoScaler);
|
||||||
} else {
|
} else {
|
||||||
await this.KubernetesHorizontalPodAutoScalerService.delete(oldAutoScaler);
|
await this.KubernetesHorizontalPodAutoScalerService.delete(oldAutoScaler);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import _ from 'lodash-es';
|
||||||
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
|
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
|
||||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||||
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
|
||||||
import { KubernetesPortainerApplicationStackNameLabel } from 'Kubernetes/models/application/models';
|
import { KubernetesPortainerApplicationStackNameLabel } from 'Kubernetes/models/application/models';
|
||||||
import { confirmDelete } from '@@/modals/confirm';
|
import { confirmDelete } from '@@/modals/confirm';
|
||||||
import { getDeploymentOptions } from '@/react/portainer/environments/environment.service';
|
import { getDeploymentOptions } from '@/react/portainer/environments/environment.service';
|
||||||
|
@ -90,7 +90,7 @@ class KubernetesApplicationsController {
|
||||||
let actionCount = selectedItems.length;
|
let actionCount = selectedItems.length;
|
||||||
for (const application of selectedItems) {
|
for (const application of selectedItems) {
|
||||||
try {
|
try {
|
||||||
if (application.ApplicationType === KubernetesApplicationTypes.HELM) {
|
if (application.ApplicationType === KubernetesApplicationTypes.Helm) {
|
||||||
await this.HelmService.uninstall(this.endpoint.Id, application);
|
await this.HelmService.uninstall(this.endpoint.Id, application);
|
||||||
} else {
|
} else {
|
||||||
await this.KubernetesApplicationService.delete(application);
|
await this.KubernetesApplicationService.delete(application);
|
||||||
|
|
|
@ -103,9 +103,16 @@
|
||||||
></kube-services-form>
|
></kube-services-form>
|
||||||
</div>
|
</div>
|
||||||
<!-- kubernetes services options -->
|
<!-- kubernetes services options -->
|
||||||
<kubernetes-summary-view form-values="ctrl.formValues" old-form-values="ctrl.savedFormValues"></kubernetes-summary-view>
|
<!-- kubernetes summary for external application -->
|
||||||
<!-- #region ACTIONS -->
|
<application-summary-section
|
||||||
|
application-kind="ctrl.formValues.ApplicationType"
|
||||||
|
form-values="ctrl.formValues"
|
||||||
|
old-form-values="ctrl.savedFormValues"
|
||||||
|
ng-if="ctrl.isExternalApplication()"
|
||||||
|
></application-summary-section>
|
||||||
|
<!-- kubernetes summary for external application -->
|
||||||
<div class="col-sm-12 form-section-title !mt-6" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT" ng-hide="ctrl.stack.IsComposeFormat"> Actions </div>
|
<div class="col-sm-12 form-section-title !mt-6" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT" ng-hide="ctrl.stack.IsComposeFormat"> Actions </div>
|
||||||
|
<!-- #region ACTIONS -->
|
||||||
<div class="form-group" ng-hide="ctrl.stack.IsComposeFormat">
|
<div class="form-group" ng-hide="ctrl.stack.IsComposeFormat">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button
|
<button
|
||||||
|
@ -423,12 +430,12 @@
|
||||||
|
|
||||||
<!-- #region DATA ACCESS POLICY -->
|
<!-- #region DATA ACCESS POLICY -->
|
||||||
<div ng-if="ctrl.showDataAccessPolicySection()">
|
<div ng-if="ctrl.showDataAccessPolicySection()">
|
||||||
<data-access-policy-form-section
|
<access-policy-form-section
|
||||||
value="ctrl.formValues.DataAccessPolicy"
|
value="ctrl.formValues.DataAccessPolicy"
|
||||||
on-change="(ctrl.onDataAccessPolicyChange)"
|
on-change="(ctrl.onDataAccessPolicyChange)"
|
||||||
is-edit="ctrl.state.isEdit"
|
is-edit="ctrl.state.isEdit"
|
||||||
persisted-folders-use-existing-volumes="ctrl.state.persistedFoldersUseExistingVolumes"
|
persisted-folders-use-existing-volumes="ctrl.state.persistedFoldersUseExistingVolumes"
|
||||||
></data-access-policy-form-section>
|
></access-policy-form-section>
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
@ -451,7 +458,7 @@
|
||||||
></app-deployment-type-form-section>
|
></app-deployment-type-form-section>
|
||||||
|
|
||||||
<!-- replica count -->
|
<!-- replica count -->
|
||||||
<div ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.REPLICATED">
|
<div ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.Replicated">
|
||||||
<replication-form-section
|
<replication-form-section
|
||||||
values="{replicaCount: ctrl.formValues.ReplicaCount}"
|
values="{replicaCount: ctrl.formValues.ReplicaCount}"
|
||||||
on-change="(ctrl.onChangeReplicaCount)"
|
on-change="(ctrl.onChangeReplicaCount)"
|
||||||
|
@ -466,9 +473,9 @@
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- #region AUTO SCALING -->
|
<!-- #region AUTO SCALING -->
|
||||||
<div ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL">
|
<div ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.Global">
|
||||||
<auto-scaling-form-section
|
<auto-scaling-form-section
|
||||||
values="{isUsed: ctrl.formValues.AutoScaler.IsUsed, minReplicas: ctrl.formValues.AutoScaler.MinReplicas, maxReplicas: ctrl.formValues.AutoScaler.MaxReplicas, targetCpuUtilizationPercentage: ctrl.formValues.AutoScaler.TargetCPUUtilization}"
|
values="ctrl.formValues.AutoScaler"
|
||||||
on-change="(ctrl.onAutoScaleChange)"
|
on-change="(ctrl.onAutoScaleChange)"
|
||||||
validation-data="{autoScalerOverflow: ctrl.autoScalerOverflow()}"
|
validation-data="{autoScalerOverflow: ctrl.autoScalerOverflow()}"
|
||||||
is-metrics-enabled="ctrl.state.useServerMetrics"
|
is-metrics-enabled="ctrl.state.useServerMetrics"
|
||||||
|
@ -478,7 +485,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<placement-form-section
|
<placement-form-section
|
||||||
ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.REPLICATED"
|
ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.Replicated"
|
||||||
values="{placements: ctrl.formValues.Placements, placementType: ctrl.formValues.PlacementType}"
|
values="{placements: ctrl.formValues.Placements, placementType: ctrl.formValues.PlacementType}"
|
||||||
on-change="(ctrl.onChangePlacements)"
|
on-change="(ctrl.onChangePlacements)"
|
||||||
></placement-form-section>
|
></placement-form-section>
|
||||||
|
@ -497,11 +504,12 @@
|
||||||
></kube-services-form>
|
></kube-services-form>
|
||||||
</div>
|
</div>
|
||||||
<!-- kubernetes services options -->
|
<!-- kubernetes services options -->
|
||||||
<kubernetes-summary-view
|
<application-summary-section
|
||||||
|
application-kind="ctrl.formValues.ApplicationType"
|
||||||
ng-if="!(!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity)"
|
ng-if="!(!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity)"
|
||||||
form-values="ctrl.formValues"
|
form-values="ctrl.formValues"
|
||||||
old-form-values="ctrl.savedFormValues"
|
old-form-values="ctrl.savedFormValues"
|
||||||
></kubernetes-summary-view>
|
></application-summary-section>
|
||||||
</div>
|
</div>
|
||||||
<!-- #region ACTIONS -->
|
<!-- #region ACTIONS -->
|
||||||
<div class="col-sm-12 form-section-title !mt-6" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT" ng-hide="ctrl.stack.IsComposeFormat"> Actions </div>
|
<div class="col-sm-12 form-section-title !mt-6" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT" ng-hide="ctrl.stack.IsComposeFormat"> Actions </div>
|
||||||
|
|
|
@ -10,17 +10,11 @@ import { getGlobalDeploymentOptions } from '@/react/portainer/settings/settings.
|
||||||
import {
|
import {
|
||||||
KubernetesApplicationDataAccessPolicies,
|
KubernetesApplicationDataAccessPolicies,
|
||||||
KubernetesApplicationDeploymentTypes,
|
KubernetesApplicationDeploymentTypes,
|
||||||
KubernetesApplicationPublishingTypes,
|
KubernetesApplicationServiceTypes,
|
||||||
KubernetesApplicationQuotaDefaults,
|
|
||||||
KubernetesApplicationTypes,
|
KubernetesApplicationTypes,
|
||||||
KubernetesDeploymentTypes,
|
} from 'Kubernetes/models/application/models/appConstants';
|
||||||
} from 'Kubernetes/models/application/models';
|
import { KubernetesApplicationQuotaDefaults, KubernetesDeploymentTypes } from 'Kubernetes/models/application/models';
|
||||||
import {
|
import { KubernetesApplicationEnvironmentVariableFormValue, KubernetesApplicationFormValues, KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||||
KubernetesApplicationEnvironmentVariableFormValue,
|
|
||||||
KubernetesApplicationFormValues,
|
|
||||||
KubernetesApplicationPersistedFolderFormValue,
|
|
||||||
KubernetesFormValidationReferences,
|
|
||||||
} from 'Kubernetes/models/application/formValues';
|
|
||||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||||
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
|
@ -76,7 +70,7 @@ class KubernetesCreateApplicationController {
|
||||||
|
|
||||||
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||||
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
||||||
this.ApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
|
this.KubernetesApplicationServiceTypes = KubernetesApplicationServiceTypes;
|
||||||
this.ApplicationTypes = KubernetesApplicationTypes;
|
this.ApplicationTypes = KubernetesApplicationTypes;
|
||||||
this.ServiceTypes = KubernetesServiceTypes;
|
this.ServiceTypes = KubernetesServiceTypes;
|
||||||
this.KubernetesDeploymentTypes = KubernetesDeploymentTypes;
|
this.KubernetesDeploymentTypes = KubernetesDeploymentTypes;
|
||||||
|
@ -151,6 +145,9 @@ class KubernetesCreateApplicationController {
|
||||||
this.onChangeReplicaCount = this.onChangeReplicaCount.bind(this);
|
this.onChangeReplicaCount = this.onChangeReplicaCount.bind(this);
|
||||||
this.onAutoScaleChange = this.onAutoScaleChange.bind(this);
|
this.onAutoScaleChange = this.onAutoScaleChange.bind(this);
|
||||||
this.onChangePlacements = this.onChangePlacements.bind(this);
|
this.onChangePlacements = this.onChangePlacements.bind(this);
|
||||||
|
this.updateApplicationType = this.updateApplicationType.bind(this);
|
||||||
|
this.getAppType = this.getAppType.bind(this);
|
||||||
|
this.showDataAccessPolicySection = this.showDataAccessPolicySection.bind(this);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
@ -165,6 +162,24 @@ class KubernetesCreateApplicationController {
|
||||||
this.$scope.$evalAsync(() => {
|
this.$scope.$evalAsync(() => {
|
||||||
this.formValues.DeploymentType = value;
|
this.formValues.DeploymentType = value;
|
||||||
});
|
});
|
||||||
|
this.updateApplicationType();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateApplicationType() {
|
||||||
|
return this.$scope.$evalAsync(() => {
|
||||||
|
this.formValues.ApplicationType = this.getAppType();
|
||||||
|
this.validatePersistedFolders();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAppType() {
|
||||||
|
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.Global) {
|
||||||
|
return this.ApplicationTypes.DaemonSet;
|
||||||
|
}
|
||||||
|
if (this.formValues.PersistedFolders && this.formValues.PersistedFolders.length) {
|
||||||
|
return this.ApplicationTypes.StatefulSet;
|
||||||
|
}
|
||||||
|
return this.ApplicationTypes.Deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeFileContent(value) {
|
onChangeFileContent(value) {
|
||||||
|
@ -230,28 +245,23 @@ class KubernetesCreateApplicationController {
|
||||||
|
|
||||||
/* #region AUTO SCALER UI MANAGEMENT */
|
/* #region AUTO SCALER UI MANAGEMENT */
|
||||||
unselectAutoScaler() {
|
unselectAutoScaler() {
|
||||||
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL) {
|
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.Global) {
|
||||||
this.formValues.AutoScaler.IsUsed = false;
|
this.formValues.AutoScaler.isUsed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAutoScaleChange(values) {
|
onAutoScaleChange(values) {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
if (!this.formValues.AutoScaler.IsUsed && values.isUsed) {
|
if (!this.formValues.AutoScaler.isUsed && values.isUsed) {
|
||||||
this.formValues.AutoScaler = {
|
this.formValues.AutoScaler = {
|
||||||
IsUsed: values.isUsed,
|
isUsed: values.isUsed,
|
||||||
MinReplicas: 1,
|
minReplicas: 1,
|
||||||
MaxReplicas: 3,
|
maxReplicas: 3,
|
||||||
TargetCPUUtilization: 50,
|
targetCpuUtilizationPercentage: 50,
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.formValues.AutoScaler = {
|
this.formValues.AutoScaler = values;
|
||||||
IsUsed: values.isUsed,
|
|
||||||
MinReplicas: values.minReplicas,
|
|
||||||
MaxReplicas: values.maxReplicas,
|
|
||||||
TargetCPUUtilization: values.targetCpuUtilizationPercentage,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
@ -319,22 +329,6 @@ class KubernetesCreateApplicationController {
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
/* #region PERSISTENT FOLDERS UI MANAGEMENT */
|
/* #region PERSISTENT FOLDERS UI MANAGEMENT */
|
||||||
addPersistedFolder() {
|
|
||||||
let storageClass = {};
|
|
||||||
if (this.storageClasses.length > 0) {
|
|
||||||
storageClass = this.storageClasses[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPf = new KubernetesApplicationPersistedFolderFormValue(storageClass);
|
|
||||||
this.formValues.PersistedFolders.push(newPf);
|
|
||||||
this.resetDeploymentType();
|
|
||||||
}
|
|
||||||
|
|
||||||
restorePersistedFolder(index) {
|
|
||||||
this.formValues.PersistedFolders[index].needsDeletion = false;
|
|
||||||
this.validatePersistedFolders();
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPersistedFolders() {
|
resetPersistedFolders() {
|
||||||
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||||
persistedFolder.existingVolume = null;
|
persistedFolder.existingVolume = null;
|
||||||
|
@ -342,32 +336,6 @@ class KubernetesCreateApplicationController {
|
||||||
});
|
});
|
||||||
this.validatePersistedFolders();
|
this.validatePersistedFolders();
|
||||||
}
|
}
|
||||||
|
|
||||||
removePersistedFolder(index) {
|
|
||||||
if (this.state.isEdit && this.formValues.PersistedFolders[index].persistentVolumeClaimName) {
|
|
||||||
this.formValues.PersistedFolders[index].needsDeletion = true;
|
|
||||||
} else {
|
|
||||||
this.formValues.PersistedFolders.splice(index, 1);
|
|
||||||
}
|
|
||||||
this.validatePersistedFolders();
|
|
||||||
}
|
|
||||||
|
|
||||||
useNewVolume(index) {
|
|
||||||
this.formValues.PersistedFolders[index].useNewVolume = true;
|
|
||||||
this.formValues.PersistedFolders[index].existingVolume = null;
|
|
||||||
this.state.persistedFoldersUseExistingVolumes = _.some(this.formValues.PersistedFolders, { useNewVolume: false });
|
|
||||||
this.validatePersistedFolders();
|
|
||||||
}
|
|
||||||
|
|
||||||
useExistingVolume(index) {
|
|
||||||
this.formValues.PersistedFolders[index].useNewVolume = false;
|
|
||||||
this.state.persistedFoldersUseExistingVolumes = _.some(this.formValues.PersistedFolders, { useNewVolume: false });
|
|
||||||
if (this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED) {
|
|
||||||
this.formValues.DataAccessPolicy = this.ApplicationDataAccessPolicies.SHARED;
|
|
||||||
this.resetDeploymentType();
|
|
||||||
}
|
|
||||||
this.validatePersistedFolders();
|
|
||||||
}
|
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
/* #region PERSISTENT FOLDERS ON CHANGE VALIDATION */
|
/* #region PERSISTENT FOLDERS ON CHANGE VALIDATION */
|
||||||
|
@ -389,7 +357,14 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangePersistedFolder(values) {
|
onChangePersistedFolder(values) {
|
||||||
this.formValues.PersistedFolders = values;
|
this.$scope.$evalAsync(() => {
|
||||||
|
this.formValues.PersistedFolders = values;
|
||||||
|
if (values && values.length && !this.supportGlobalDeployment()) {
|
||||||
|
this.onChangeDeploymentType(this.ApplicationDeploymentTypes.Replicated);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updateApplicationType();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeExistingVolumeSelection() {
|
onChangeExistingVolumeSelection() {
|
||||||
|
@ -445,13 +420,12 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
resetDeploymentType() {
|
resetDeploymentType() {
|
||||||
this.formValues.DeploymentType = this.ApplicationDeploymentTypes.REPLICATED;
|
this.formValues.DeploymentType = this.ApplicationDeploymentTypes.Replicated;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The data access policy panel is not shown when:
|
// // The data access policy panel is shown when a persisted folder is specified
|
||||||
// * There is not persisted folder specified
|
|
||||||
showDataAccessPolicySection() {
|
showDataAccessPolicySection() {
|
||||||
return this.formValues.PersistedFolders.length !== 0;
|
return this.formValues.PersistedFolders.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A global deployment is not available when either:
|
// A global deployment is not available when either:
|
||||||
|
@ -460,7 +434,7 @@ class KubernetesCreateApplicationController {
|
||||||
supportGlobalDeployment() {
|
supportGlobalDeployment() {
|
||||||
const hasFolders = this.formValues.PersistedFolders.length !== 0;
|
const hasFolders = this.formValues.PersistedFolders.length !== 0;
|
||||||
const hasRWOOnly = KubernetesApplicationHelper.hasRWOOnly(this.formValues);
|
const hasRWOOnly = KubernetesApplicationHelper.hasRWOOnly(this.formValues);
|
||||||
const isIsolated = this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED;
|
const isIsolated = this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.Isolated;
|
||||||
|
|
||||||
if (hasFolders && (hasRWOOnly || isIsolated)) {
|
if (hasFolders && (hasRWOOnly || isIsolated)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -469,9 +443,9 @@ class KubernetesCreateApplicationController {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A StatefulSet is defined by DataAccessPolicy === ISOLATED
|
// A StatefulSet is defined by DataAccessPolicy === 'Isolated'
|
||||||
isEditAndStatefulSet() {
|
isEditAndStatefulSet() {
|
||||||
return this.state.isEdit && this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED;
|
return this.state.isEdit && this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.Isolated;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A scalable deployment is available when either:
|
// A scalable deployment is available when either:
|
||||||
|
@ -482,7 +456,7 @@ class KubernetesCreateApplicationController {
|
||||||
supportScalableReplicaDeployment() {
|
supportScalableReplicaDeployment() {
|
||||||
const hasFolders = this.formValues.PersistedFolders.length !== 0;
|
const hasFolders = this.formValues.PersistedFolders.length !== 0;
|
||||||
const hasRWOOnly = KubernetesApplicationHelper.hasRWOOnly(this.formValues);
|
const hasRWOOnly = KubernetesApplicationHelper.hasRWOOnly(this.formValues);
|
||||||
const isIsolated = this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED;
|
const isIsolated = this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.Isolated;
|
||||||
|
|
||||||
if (!hasFolders || isIsolated || (hasFolders && !hasRWOOnly)) {
|
if (!hasFolders || isIsolated || (hasFolders && !hasRWOOnly)) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -540,7 +514,7 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
effectiveInstances() {
|
effectiveInstances() {
|
||||||
return this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL ? this.nodeNumber : this.formValues.ReplicaCount;
|
return this.formValues.DeploymentType === this.ApplicationDeploymentTypes.Global ? this.nodeNumber : this.formValues.ReplicaCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPortErrors() {
|
hasPortErrors() {
|
||||||
|
@ -564,11 +538,11 @@ class KubernetesCreateApplicationController {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.REPLICATED) {
|
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.Replicated) {
|
||||||
return this.nodesLimits.overflowForReplica(cpu, memory, instances);
|
return this.nodesLimits.overflowForReplica(cpu, memory, instances);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeploymentType == GLOBAL
|
// DeploymentType == 'Global'
|
||||||
return this.nodesLimits.overflowForGlobal(cpu, memory);
|
return this.nodesLimits.overflowForGlobal(cpu, memory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -613,7 +587,7 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
isExistingVolumeButtonDisabled() {
|
isExistingVolumeButtonDisabled() {
|
||||||
return !this.hasAvailableVolumes() || (this.isEdit && this.application.ApplicationType === this.ApplicationTypes.STATEFULSET);
|
return !this.hasAvailableVolumes() || (this.isEdit && this.application.ApplicationType === this.ApplicationTypes.StatefulSet);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
@ -630,7 +604,7 @@ class KubernetesCreateApplicationController {
|
||||||
const scalable = this.supportScalableReplicaDeployment();
|
const scalable = this.supportScalableReplicaDeployment();
|
||||||
const global = this.supportGlobalDeployment();
|
const global = this.supportGlobalDeployment();
|
||||||
const replica = this.formValues.ReplicaCount > 1;
|
const replica = this.formValues.ReplicaCount > 1;
|
||||||
const replicated = this.formValues.DeploymentType === this.ApplicationDeploymentTypes.REPLICATED;
|
const replicated = this.formValues.DeploymentType === this.ApplicationDeploymentTypes.Replicated;
|
||||||
const res = (replicated && !scalable && replica) || (!replicated && !global);
|
const res = (replicated && !scalable && replica) || (!replicated && !global);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
@ -784,7 +758,7 @@ class KubernetesCreateApplicationController {
|
||||||
if (this.savedFormValues) {
|
if (this.savedFormValues) {
|
||||||
this.formValues.PublishingType = this.savedFormValues.PublishingType;
|
this.formValues.PublishingType = this.savedFormValues.PublishingType;
|
||||||
} else {
|
} else {
|
||||||
this.formValues.PublishingType = this.ApplicationPublishingTypes.CLUSTER_IP;
|
this.formValues.PublishingType = this.KubernetesApplicationServiceTypes.ClusterIP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.formValues.OriginalIngresses = this.ingresses;
|
this.formValues.OriginalIngresses = this.ingresses;
|
||||||
|
@ -1070,9 +1044,8 @@ class KubernetesCreateApplicationController {
|
||||||
this.savedFormValues = angular.copy(this.formValues);
|
this.savedFormValues = angular.copy(this.formValues);
|
||||||
this.updateNamespaceLimits(this.namespaceWithQuota);
|
this.updateNamespaceLimits(this.namespaceWithQuota);
|
||||||
this.updateSliders(this.namespaceWithQuota);
|
this.updateSliders(this.namespaceWithQuota);
|
||||||
delete this.formValues.ApplicationType;
|
|
||||||
|
|
||||||
if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) {
|
if (this.application.ApplicationType !== KubernetesApplicationTypes.StatefulSet) {
|
||||||
_.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
_.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||||
const volume = _.find(this.availableVolumes, ['PersistentVolumeClaim.Name', persistedFolder.persistentVolumeClaimName]);
|
const volume = _.find(this.availableVolumes, ['PersistentVolumeClaim.Name', persistedFolder.persistentVolumeClaimName]);
|
||||||
if (volume) {
|
if (volume) {
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import { KubernetesResourceTypes, KubernetesResourceActions } from 'Kubernetes/models/resource-types/models';
|
import { KubernetesResourceTypes, KubernetesResourceActions } from 'Kubernetes/models/resource-types/models';
|
||||||
import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/formValues';
|
import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/formValues';
|
||||||
import { KubernetesDeployment } from 'Kubernetes/models/deployment/models';
|
|
||||||
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
|
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
|
||||||
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
|
|
||||||
import { KubernetesService, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
import { KubernetesService, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||||
import { KubernetesApplication, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models/appConstants';
|
||||||
import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper';
|
|
||||||
import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter';
|
import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter';
|
||||||
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
||||||
import KubernetesServiceConverter from 'Kubernetes/converters/service';
|
import KubernetesServiceConverter from 'Kubernetes/converters/service';
|
||||||
|
@ -33,7 +30,7 @@ export function getApplicationResources(formValues, oldFormValues = {}) {
|
||||||
* Get summary of Kubernetes resources to be created
|
* Get summary of Kubernetes resources to be created
|
||||||
* @param {KubernetesApplicationFormValues} formValues
|
* @param {KubernetesApplicationFormValues} formValues
|
||||||
*/
|
*/
|
||||||
function getCreatedApplicationResources(formValues) {
|
export function getCreatedApplicationResources(formValues) {
|
||||||
const resources = [];
|
const resources = [];
|
||||||
|
|
||||||
let [app, headlessService, services, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
let [app, headlessService, services, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
||||||
|
@ -65,14 +62,14 @@ function getCreatedApplicationResources(formValues) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal pod autoscalers
|
// Horizontal pod autoscalers
|
||||||
if (formValues.AutoScaler.IsUsed && formValues.DeploymentType !== KubernetesApplicationDeploymentTypes.GLOBAL) {
|
if (formValues.AutoScaler.IsUsed && formValues.DeploymentType !== KubernetesApplicationDeploymentTypes.Global) {
|
||||||
const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app);
|
const kind = app.ApplicationType;
|
||||||
const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind);
|
const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind);
|
||||||
resources.push({ action: CREATE, kind: KubernetesResourceTypes.HORIZONTAL_POD_AUTOSCALER, name: autoScaler.Name });
|
resources.push({ action: CREATE, kind: KubernetesResourceTypes.HORIZONTAL_POD_AUTOSCALER, name: autoScaler.Name });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deployment
|
// Deployment
|
||||||
const appResourceType = getApplicationResourceType(app);
|
const appResourceType = app.ApplicationType;
|
||||||
if (appResourceType !== null) {
|
if (appResourceType !== null) {
|
||||||
resources.push({ action: CREATE, kind: appResourceType, name: app.Name });
|
resources.push({ action: CREATE, kind: appResourceType, name: app.Name });
|
||||||
}
|
}
|
||||||
|
@ -85,13 +82,13 @@ function getCreatedApplicationResources(formValues) {
|
||||||
* @param {KubernetesApplicationFormValues} oldFormValues
|
* @param {KubernetesApplicationFormValues} oldFormValues
|
||||||
* @param {KubernetesApplicationFormValues} newFormValues
|
* @param {KubernetesApplicationFormValues} newFormValues
|
||||||
*/
|
*/
|
||||||
function getUpdatedApplicationResources(oldFormValues, newFormValues) {
|
export function getUpdatedApplicationResources(oldFormValues, newFormValues) {
|
||||||
const resources = [];
|
const resources = [];
|
||||||
|
|
||||||
const [oldApp, oldHeadlessService, oldServices, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
|
const [oldApp, oldHeadlessService, oldServices, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
|
||||||
const [newApp, newHeadlessService, newServices, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
|
const [newApp, newHeadlessService, newServices, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
|
||||||
const oldAppResourceType = getApplicationResourceType(oldApp);
|
const oldAppResourceType = oldApp.ApplicationType;
|
||||||
const newAppResourceType = getApplicationResourceType(newApp);
|
const newAppResourceType = newApp.ApplicationType;
|
||||||
|
|
||||||
if (oldAppResourceType !== newAppResourceType) {
|
if (oldAppResourceType !== newAppResourceType) {
|
||||||
// Deployment
|
// Deployment
|
||||||
|
@ -152,7 +149,7 @@ function getUpdatedApplicationResources(oldFormValues, newFormValues) {
|
||||||
resources.push({ action: DELETE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP });
|
resources.push({ action: DELETE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp);
|
const newKind = newApp.ApplicationType;
|
||||||
const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind);
|
const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind);
|
||||||
if (!oldFormValues.AutoScaler.IsUsed) {
|
if (!oldFormValues.AutoScaler.IsUsed) {
|
||||||
if (newFormValues.AutoScaler.IsUsed) {
|
if (newFormValues.AutoScaler.IsUsed) {
|
||||||
|
@ -161,7 +158,7 @@ function getUpdatedApplicationResources(oldFormValues, newFormValues) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Horizontal pod autoscalers
|
// Horizontal pod autoscalers
|
||||||
const oldKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(oldApp);
|
const oldKind = oldApp.ApplicationType;
|
||||||
const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind);
|
const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind);
|
||||||
if (newFormValues.AutoScaler.IsUsed) {
|
if (newFormValues.AutoScaler.IsUsed) {
|
||||||
const hpaUpdateSummary = getHorizontalPodAutoScalerUpdateResourceSummary(oldAutoScaler, newAutoScaler);
|
const hpaUpdateSummary = getHorizontalPodAutoScalerUpdateResourceSummary(oldAutoScaler, newAutoScaler);
|
||||||
|
@ -176,17 +173,6 @@ function getUpdatedApplicationResources(oldFormValues, newFormValues) {
|
||||||
return resources;
|
return resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getApplicationResourceType(app) {
|
|
||||||
if (app instanceof KubernetesDeployment || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT)) {
|
|
||||||
return KubernetesResourceTypes.DEPLOYMENT;
|
|
||||||
} else if (app instanceof KubernetesDaemonSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET)) {
|
|
||||||
return KubernetesResourceTypes.DAEMONSET;
|
|
||||||
} else if (app instanceof KubernetesStatefulSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET)) {
|
|
||||||
return KubernetesResourceTypes.STATEFULSET;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIngressUpdateSummary(oldIngresses, newIngresses) {
|
function getIngressUpdateSummary(oldIngresses, newIngresses) {
|
||||||
const ingressesSummaries = newIngresses
|
const ingressesSummaries = newIngresses
|
||||||
.map((newIng) => {
|
.map((newIng) => {
|
||||||
|
|
|
@ -5,15 +5,15 @@ import { TextTip } from '@@/Tip/TextTip';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
|
|
||||||
import { convertToArrayOfStrings, parseDotEnvFile } from './utils';
|
import { convertToArrayOfStrings, parseDotEnvFile } from './utils';
|
||||||
import { type Value } from './types';
|
import { type Values } from './types';
|
||||||
|
|
||||||
export function AdvancedMode({
|
export function AdvancedMode({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onSimpleModeClick,
|
onSimpleModeClick,
|
||||||
}: {
|
}: {
|
||||||
value: Value;
|
value: Values;
|
||||||
onChange: (value: Value) => void;
|
onChange: (value: Values) => void;
|
||||||
onSimpleModeClick: () => void;
|
onSimpleModeClick: () => void;
|
||||||
}) {
|
}) {
|
||||||
const editorValue = convertToArrayOfStrings(value).join('\n');
|
const editorValue = convertToArrayOfStrings(value).join('\n');
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { buildUniquenessTest } from '../validate-unique';
|
||||||
|
|
||||||
import { AdvancedMode } from './AdvancedMode';
|
import { AdvancedMode } from './AdvancedMode';
|
||||||
import { SimpleMode } from './SimpleMode';
|
import { SimpleMode } from './SimpleMode';
|
||||||
import { Value } from './types';
|
import { Values } from './types';
|
||||||
|
|
||||||
export function EnvironmentVariablesFieldset({
|
export function EnvironmentVariablesFieldset({
|
||||||
onChange,
|
onChange,
|
||||||
|
@ -14,9 +14,9 @@ export function EnvironmentVariablesFieldset({
|
||||||
errors,
|
errors,
|
||||||
canUndoDelete,
|
canUndoDelete,
|
||||||
}: {
|
}: {
|
||||||
values: Value;
|
values: Values;
|
||||||
onChange(value: Value): void;
|
onChange(value: Values): void;
|
||||||
errors?: ArrayError<Value>;
|
errors?: ArrayError<Values>;
|
||||||
canUndoDelete?: boolean;
|
canUndoDelete?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [simpleMode, setSimpleMode] = useState(true);
|
const [simpleMode, setSimpleMode] = useState(true);
|
||||||
|
@ -42,7 +42,7 @@ export function EnvironmentVariablesFieldset({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function envVarValidation(): SchemaOf<Value> {
|
export function envVarValidation(): SchemaOf<Values> {
|
||||||
return array(
|
return array(
|
||||||
object({
|
object({
|
||||||
name: string().required('Name is required'),
|
name: string().required('Name is required'),
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
import { ArrayError } from '../InputList/InputList';
|
import { ArrayError } from '../InputList/InputList';
|
||||||
|
|
||||||
import { Value } from './types';
|
import { Values } from './types';
|
||||||
import { EnvironmentVariablesFieldset } from './EnvironmentVariablesFieldset';
|
import { EnvironmentVariablesFieldset } from './EnvironmentVariablesFieldset';
|
||||||
|
|
||||||
export function EnvironmentVariablesPanel({
|
export function EnvironmentVariablesPanel({
|
||||||
|
@ -15,10 +15,10 @@ export function EnvironmentVariablesPanel({
|
||||||
isFoldable = false,
|
isFoldable = false,
|
||||||
}: {
|
}: {
|
||||||
explanation?: string;
|
explanation?: string;
|
||||||
values: Value;
|
values: Values;
|
||||||
onChange(value: Value): void;
|
onChange(value: Values): void;
|
||||||
showHelpMessage?: boolean;
|
showHelpMessage?: boolean;
|
||||||
errors?: ArrayError<Value>;
|
errors?: ArrayError<Values>;
|
||||||
isFoldable?: boolean;
|
isFoldable?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { FileUploadField } from '@@/form-components/FileUpload';
|
||||||
import { InputList } from '@@/form-components/InputList';
|
import { InputList } from '@@/form-components/InputList';
|
||||||
import { ArrayError } from '@@/form-components/InputList/InputList';
|
import { ArrayError } from '@@/form-components/InputList/InputList';
|
||||||
|
|
||||||
import type { Value } from './types';
|
import type { Values } from './types';
|
||||||
import { parseDotEnvFile } from './utils';
|
import { parseDotEnvFile } from './utils';
|
||||||
import { EnvironmentVariableItem } from './EnvironmentVariableItem';
|
import { EnvironmentVariableItem } from './EnvironmentVariableItem';
|
||||||
|
|
||||||
|
@ -20,10 +20,10 @@ export function SimpleMode({
|
||||||
errors,
|
errors,
|
||||||
canUndoDelete,
|
canUndoDelete,
|
||||||
}: {
|
}: {
|
||||||
value: Value;
|
value: Values;
|
||||||
onChange: (value: Value) => void;
|
onChange: (value: Values) => void;
|
||||||
onAdvancedModeClick: () => void;
|
onAdvancedModeClick: () => void;
|
||||||
errors?: ArrayError<Value>;
|
errors?: ArrayError<Values>;
|
||||||
canUndoDelete?: boolean;
|
canUndoDelete?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
@ -70,7 +70,7 @@ export function SimpleMode({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FileEnv({ onChooseFile }: { onChooseFile: (file: Value) => void }) {
|
function FileEnv({ onChooseFile }: { onChooseFile: (file: Values) => void }) {
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
const fileTooBig = file && file.size > 1024 * 1024;
|
const fileTooBig = file && file.size > 1024 * 1024;
|
||||||
|
|
|
@ -5,4 +5,4 @@ export {
|
||||||
|
|
||||||
export { EnvironmentVariablesPanel } from './EnvironmentVariablesPanel';
|
export { EnvironmentVariablesPanel } from './EnvironmentVariablesPanel';
|
||||||
|
|
||||||
export { type Value as EnvVarValues } from './types';
|
export { type Values as EnvVarValues } from './types';
|
||||||
|
|
|
@ -4,4 +4,4 @@ export interface EnvVar {
|
||||||
needsDeletion?: boolean;
|
needsDeletion?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Value = Array<EnvVar>;
|
export type Values = Array<EnvVar>;
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import { EnvironmentVariablesPanel } from '@@/form-components/EnvironmentVariablesFieldset';
|
import {
|
||||||
|
EnvVarValues,
|
||||||
|
EnvironmentVariablesPanel,
|
||||||
|
} from '@@/form-components/EnvironmentVariablesFieldset';
|
||||||
import { ArrayError } from '@@/form-components/InputList/InputList';
|
import { ArrayError } from '@@/form-components/InputList/InputList';
|
||||||
|
|
||||||
import { Values } from './types';
|
|
||||||
|
|
||||||
export function EnvVarsTab({
|
export function EnvVarsTab({
|
||||||
values,
|
values,
|
||||||
onChange,
|
onChange,
|
||||||
errors,
|
errors,
|
||||||
}: {
|
}: {
|
||||||
values: Values;
|
values: EnvVarValues;
|
||||||
onChange(value: Values): void;
|
onChange(value: EnvVarValues): void;
|
||||||
errors?: ArrayError<Values>;
|
errors?: ArrayError<EnvVarValues>;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
@ -23,7 +24,7 @@ export function EnvVarsTab({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleChange(values: Values) {
|
function handleChange(values: EnvVarValues) {
|
||||||
onChange(values);
|
onChange(values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { toRequest } from './toRequest';
|
||||||
import { toViewModel, getDefaultViewModel } from './toViewModel';
|
import { toViewModel, getDefaultViewModel } from './toViewModel';
|
||||||
|
|
||||||
export { EnvVarsTab } from './EnvVarsTab';
|
export { EnvVarsTab } from './EnvVarsTab';
|
||||||
export type { Values } from './types';
|
|
||||||
|
|
||||||
export const envVarsTabUtils = {
|
export const envVarsTabUtils = {
|
||||||
toRequest,
|
toRequest,
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { convertToArrayOfStrings } from '@@/form-components/EnvironmentVariablesFieldset/utils';
|
import { convertToArrayOfStrings } from '@@/form-components/EnvironmentVariablesFieldset/utils';
|
||||||
|
import { EnvVarValues } from '@@/form-components/EnvironmentVariablesFieldset';
|
||||||
|
|
||||||
import { CreateContainerRequest } from '../types';
|
import { CreateContainerRequest } from '../types';
|
||||||
|
|
||||||
import { Values } from './types';
|
|
||||||
|
|
||||||
export function toRequest(
|
export function toRequest(
|
||||||
oldConfig: CreateContainerRequest,
|
oldConfig: CreateContainerRequest,
|
||||||
values: Values
|
values: EnvVarValues
|
||||||
): CreateContainerRequest {
|
): CreateContainerRequest {
|
||||||
return {
|
return {
|
||||||
...oldConfig,
|
...oldConfig,
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export type { Value as Values } from '@@/form-components/EnvironmentVariablesFieldset/types';
|
|
|
@ -32,10 +32,7 @@ import {
|
||||||
VolumesTabValues,
|
VolumesTabValues,
|
||||||
volumesTabUtils,
|
volumesTabUtils,
|
||||||
} from '@/react/docker/containers/CreateView/VolumesTab';
|
} from '@/react/docker/containers/CreateView/VolumesTab';
|
||||||
import {
|
import { envVarsTabUtils } from '@/react/docker/containers/CreateView/EnvVarsTab';
|
||||||
Values as EnvVarsTabValues,
|
|
||||||
envVarsTabUtils,
|
|
||||||
} from '@/react/docker/containers/CreateView/EnvVarsTab';
|
|
||||||
import { UserId } from '@/portainer/users/types';
|
import { UserId } from '@/portainer/users/types';
|
||||||
import { getImageConfig } from '@/react/portainer/registries/utils/getImageConfig';
|
import { getImageConfig } from '@/react/portainer/registries/utils/getImageConfig';
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
@ -43,6 +40,8 @@ import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
import { useWebhooks } from '@/react/portainer/webhooks/useWebhooks';
|
import { useWebhooks } from '@/react/portainer/webhooks/useWebhooks';
|
||||||
import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
|
import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
|
||||||
|
|
||||||
|
import { EnvVarValues } from '@@/form-components/EnvironmentVariablesFieldset';
|
||||||
|
|
||||||
import { useNetworksForSelector } from '../components/NetworkSelector';
|
import { useNetworksForSelector } from '../components/NetworkSelector';
|
||||||
import { useContainers } from '../queries/containers';
|
import { useContainers } from '../queries/containers';
|
||||||
import { useContainer } from '../queries/container';
|
import { useContainer } from '../queries/container';
|
||||||
|
@ -55,7 +54,7 @@ export interface Values extends BaseFormValues {
|
||||||
restartPolicy: RestartPolicy;
|
restartPolicy: RestartPolicy;
|
||||||
resources: ResourcesTabValues;
|
resources: ResourcesTabValues;
|
||||||
capabilities: CapabilitiesTabValues;
|
capabilities: CapabilitiesTabValues;
|
||||||
env: EnvVarsTabValues;
|
env: EnvVarValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInitialValues(submitting: boolean) {
|
export function useInitialValues(submitting: boolean) {
|
||||||
|
|
|
@ -2,11 +2,13 @@ import { BoxSelector } from '@@/BoxSelector';
|
||||||
import { FormSection } from '@@/form-components/FormSection';
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
|
import { DeploymentType } from '../types';
|
||||||
|
|
||||||
import { getDeploymentOptions } from './deploymentOptions';
|
import { getDeploymentOptions } from './deploymentOptions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: number;
|
value: DeploymentType;
|
||||||
onChange(value: number): void;
|
onChange(value: DeploymentType): void;
|
||||||
supportGlobalDeployment: boolean;
|
supportGlobalDeployment: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { Box, Boxes } from 'lucide-react';
|
import { Box, Boxes } from 'lucide-react';
|
||||||
|
|
||||||
import { KubernetesApplicationDataAccessPolicies } from '@/kubernetes/models/application/models';
|
|
||||||
|
|
||||||
import { BoxSelector, BoxSelectorOption } from '@@/BoxSelector';
|
import { BoxSelector, BoxSelectorOption } from '@@/BoxSelector';
|
||||||
|
|
||||||
|
import { AppDataAccessPolicy } from '../types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isEdit: boolean;
|
isEdit: boolean;
|
||||||
persistedFoldersUseExistingVolumes: boolean;
|
persistedFoldersUseExistingVolumes: boolean;
|
||||||
value: number;
|
value: AppDataAccessPolicy;
|
||||||
onChange(value: number): void;
|
onChange(value: AppDataAccessPolicy): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataAccessPolicyFormSection({
|
export function DataAccessPolicyFormSection({
|
||||||
|
@ -31,13 +31,13 @@ export function DataAccessPolicyFormSection({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOptions(
|
function getOptions(
|
||||||
value: number,
|
value: AppDataAccessPolicy,
|
||||||
isEdit: boolean,
|
isEdit: boolean,
|
||||||
persistedFoldersUseExistingVolumes: boolean
|
persistedFoldersUseExistingVolumes: boolean
|
||||||
): ReadonlyArray<BoxSelectorOption<number>> {
|
): ReadonlyArray<BoxSelectorOption<AppDataAccessPolicy>> {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
value: KubernetesApplicationDataAccessPolicies.ISOLATED,
|
value: 'Isolated',
|
||||||
id: 'data_access_isolated',
|
id: 'data_access_isolated',
|
||||||
icon: Boxes,
|
icon: Boxes,
|
||||||
iconType: 'badge',
|
iconType: 'badge',
|
||||||
|
@ -49,12 +49,10 @@ function getOptions(
|
||||||
? 'Changing the data access policy is not allowed'
|
? 'Changing the data access policy is not allowed'
|
||||||
: '',
|
: '',
|
||||||
disabled: () =>
|
disabled: () =>
|
||||||
(isEdit &&
|
(isEdit && value !== 'Isolated') || persistedFoldersUseExistingVolumes,
|
||||||
value !== KubernetesApplicationDataAccessPolicies.ISOLATED) ||
|
|
||||||
persistedFoldersUseExistingVolumes,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: KubernetesApplicationDataAccessPolicies.SHARED,
|
value: 'Shared',
|
||||||
id: 'data_access_shared',
|
id: 'data_access_shared',
|
||||||
icon: Box,
|
icon: Box,
|
||||||
iconType: 'badge',
|
iconType: 'badge',
|
||||||
|
@ -63,8 +61,7 @@ function getOptions(
|
||||||
'Application will be deployed as a Deployment with a shared storage access',
|
'Application will be deployed as a Deployment with a shared storage access',
|
||||||
tooltip: () =>
|
tooltip: () =>
|
||||||
isEdit ? 'Changing the data access policy is not allowed' : '',
|
isEdit ? 'Changing the data access policy is not allowed' : '',
|
||||||
disabled: () =>
|
disabled: () => isEdit && value !== 'Shared',
|
||||||
isEdit && value !== KubernetesApplicationDataAccessPolicies.SHARED,
|
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
|
|
||||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import {
|
import {
|
||||||
useIngressControllers,
|
useIngressControllers,
|
||||||
|
@ -10,12 +9,7 @@ import {
|
||||||
|
|
||||||
import { FormSection } from '@@/form-components/FormSection';
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
|
||||||
import {
|
import { ServiceFormValues, ServiceTypeOption, ServiceType } from './types';
|
||||||
ServiceFormValues,
|
|
||||||
ServiceTypeAngularEnum,
|
|
||||||
ServiceTypeOption,
|
|
||||||
ServiceTypeValue,
|
|
||||||
} from './types';
|
|
||||||
import { generateUniqueName } from './utils';
|
import { generateUniqueName } from './utils';
|
||||||
import { ClusterIpServicesForm } from './cluster-ip/ClusterIpServicesForm';
|
import { ClusterIpServicesForm } from './cluster-ip/ClusterIpServicesForm';
|
||||||
import { ServiceTabs } from './components/ServiceTabs';
|
import { ServiceTabs } from './components/ServiceTabs';
|
||||||
|
@ -24,15 +18,6 @@ import { LoadBalancerServicesForm } from './load-balancer/LoadBalancerServicesFo
|
||||||
import { ServiceTabLabel } from './components/ServiceTabLabel';
|
import { ServiceTabLabel } from './components/ServiceTabLabel';
|
||||||
import { PublishingExplaination } from './PublishingExplaination';
|
import { PublishingExplaination } from './PublishingExplaination';
|
||||||
|
|
||||||
const serviceTypeEnumsToValues: Record<
|
|
||||||
ServiceTypeAngularEnum,
|
|
||||||
ServiceTypeValue
|
|
||||||
> = {
|
|
||||||
[KubernetesApplicationPublishingTypes.CLUSTER_IP]: 'ClusterIP',
|
|
||||||
[KubernetesApplicationPublishingTypes.NODE_PORT]: 'NodePort',
|
|
||||||
[KubernetesApplicationPublishingTypes.LOAD_BALANCER]: 'LoadBalancer',
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
values: ServiceFormValues[];
|
values: ServiceFormValues[];
|
||||||
onChange: (services: ServiceFormValues[]) => void;
|
onChange: (services: ServiceFormValues[]) => void;
|
||||||
|
@ -53,7 +38,7 @@ export function KubeServicesForm({
|
||||||
namespace,
|
namespace,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [selectedServiceType, setSelectedServiceType] =
|
const [selectedServiceType, setSelectedServiceType] =
|
||||||
useState<ServiceTypeValue>('ClusterIP');
|
useState<ServiceType>('ClusterIP');
|
||||||
|
|
||||||
// start loading ingresses and controllers early to reduce perceived loading time
|
// start loading ingresses and controllers early to reduce perceived loading time
|
||||||
const environmentId = useEnvironmentId();
|
const environmentId = useEnvironmentId();
|
||||||
|
@ -195,17 +180,17 @@ function getUniqNames(appName: string, services: ServiceFormValues[]) {
|
||||||
*/
|
*/
|
||||||
function getServiceTypeCounts(
|
function getServiceTypeCounts(
|
||||||
services: ServiceFormValues[]
|
services: ServiceFormValues[]
|
||||||
): Record<ServiceTypeValue, number> {
|
): Record<ServiceType, number> {
|
||||||
return services.reduce(
|
return services.reduce(
|
||||||
(acc, service) => {
|
(acc, service) => {
|
||||||
const type = serviceTypeEnumsToValues[service.Type];
|
const type = service.Type;
|
||||||
const count = acc[type];
|
const count = acc[type];
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
[type]: count ? count + 1 : 1,
|
[type]: count ? count + 1 : 1,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{} as Record<ServiceTypeValue, number>
|
{} as Record<ServiceType, number>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,10 +200,10 @@ function getServiceTypeCounts(
|
||||||
function getServiceTypeHasErrors(
|
function getServiceTypeHasErrors(
|
||||||
services: ServiceFormValues[],
|
services: ServiceFormValues[],
|
||||||
errors: FormikErrors<ServiceFormValues[] | undefined>
|
errors: FormikErrors<ServiceFormValues[] | undefined>
|
||||||
): Record<ServiceTypeValue, boolean> {
|
): Record<ServiceType, boolean> {
|
||||||
return services.reduce(
|
return services.reduce(
|
||||||
(acc, service, index) => {
|
(acc, service, index) => {
|
||||||
const type = serviceTypeEnumsToValues[service.Type];
|
const type = service.Type;
|
||||||
const serviceHasErrors = !!errors?.[index];
|
const serviceHasErrors = !!errors?.[index];
|
||||||
// if the service type already has an error, don't overwrite it
|
// if the service type already has an error, don't overwrite it
|
||||||
if (acc[type] === true) return acc;
|
if (acc[type] === true) return acc;
|
||||||
|
@ -228,6 +213,6 @@ function getServiceTypeHasErrors(
|
||||||
[type]: serviceHasErrors,
|
[type]: serviceHasErrors,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{} as Record<ServiceTypeValue, boolean>
|
{} as Record<ServiceType, boolean>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,10 +88,7 @@ export function ClusterIpServiceForm({
|
||||||
value={servicePort.targetPort}
|
value={servicePort.targetPort}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const newServicePorts = [...servicePorts];
|
const newServicePorts = [...servicePorts];
|
||||||
const newValue =
|
const newValue = e.target.valueAsNumber;
|
||||||
e.target.value === ''
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value);
|
|
||||||
newServicePorts[portIndex] = {
|
newServicePorts[portIndex] = {
|
||||||
...newServicePorts[portIndex],
|
...newServicePorts[portIndex],
|
||||||
targetPort: newValue,
|
targetPort: newValue,
|
||||||
|
@ -113,10 +110,7 @@ export function ClusterIpServiceForm({
|
||||||
const newServicePorts = [...servicePorts];
|
const newServicePorts = [...servicePorts];
|
||||||
newServicePorts[portIndex] = {
|
newServicePorts[portIndex] = {
|
||||||
...newServicePorts[portIndex],
|
...newServicePorts[portIndex],
|
||||||
port:
|
port: e.target.valueAsNumber,
|
||||||
e.target.value === ''
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value),
|
|
||||||
};
|
};
|
||||||
onChangePort(newServicePorts);
|
onChangePort(newServicePorts);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
|
|
||||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
|
||||||
|
|
||||||
import { Card } from '@@/Card';
|
import { Card } from '@@/Card';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
|
@ -36,8 +34,7 @@ export function ClusterIpServicesForm({
|
||||||
isEditMode,
|
isEditMode,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const clusterIPServiceCount = services.filter(
|
const clusterIPServiceCount = services.filter(
|
||||||
(service) =>
|
(service) => service.Type === 'ClusterIP'
|
||||||
service.Type === KubernetesApplicationPublishingTypes.CLUSTER_IP
|
|
||||||
).length;
|
).length;
|
||||||
return (
|
return (
|
||||||
<Card className="pb-5">
|
<Card className="pb-5">
|
||||||
|
@ -50,8 +47,7 @@ export function ClusterIpServicesForm({
|
||||||
{clusterIPServiceCount > 0 && (
|
{clusterIPServiceCount > 0 && (
|
||||||
<div className="flex w-full flex-col gap-4">
|
<div className="flex w-full flex-col gap-4">
|
||||||
{services.map((service, index) =>
|
{services.map((service, index) =>
|
||||||
service.Type ===
|
service.Type === 'ClusterIP' ? (
|
||||||
KubernetesApplicationPublishingTypes.CLUSTER_IP ? (
|
|
||||||
<ClusterIpServiceForm
|
<ClusterIpServiceForm
|
||||||
key={index}
|
key={index}
|
||||||
serviceName={service.Name}
|
serviceName={service.Name}
|
||||||
|
@ -86,7 +82,7 @@ export function ClusterIpServicesForm({
|
||||||
services.length + 1,
|
services.length + 1,
|
||||||
services
|
services
|
||||||
);
|
);
|
||||||
newService.Type = KubernetesApplicationPublishingTypes.CLUSTER_IP;
|
newService.Type = 'ClusterIP';
|
||||||
const newServicePort = newPort(newService.Name);
|
const newServicePort = newPort(newService.Name);
|
||||||
newService.Ports = [newServicePort];
|
newService.Ports = [newServicePort];
|
||||||
newService.Selector = selector;
|
newService.Selector = selector;
|
||||||
|
|
|
@ -22,7 +22,7 @@ export function ContainerPortInput({
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control min-w-max"
|
className="form-control min-w-max"
|
||||||
name={`container_port_${portIndex}`}
|
name={`container_port_${portIndex}`}
|
||||||
placeholder="80"
|
placeholder="e.g. 80"
|
||||||
min="1"
|
min="1"
|
||||||
max="65535"
|
max="65535"
|
||||||
value={value ?? ''}
|
value={value ?? ''}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export function ServicePortInput({
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control min-w-max"
|
className="form-control min-w-max"
|
||||||
name={`service_port_${portIndex}`}
|
name={`service_port_${portIndex}`}
|
||||||
placeholder="80"
|
placeholder="e.g. 80"
|
||||||
min="1"
|
min="1"
|
||||||
max="65535"
|
max="65535"
|
||||||
value={value ?? ''}
|
value={value ?? ''}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { ServiceTypeOption, ServiceTypeValue } from '../types';
|
import { ServiceTypeOption, ServiceType } from '../types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
serviceTypeOptions: ServiceTypeOption[];
|
serviceTypeOptions: ServiceTypeOption[];
|
||||||
selectedServiceType: ServiceTypeValue;
|
selectedServiceType: ServiceType;
|
||||||
setSelectedServiceType: (serviceTypeValue: ServiceTypeValue) => void;
|
setSelectedServiceType: (serviceTypeValue: ServiceType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ServiceTabs({
|
export function ServiceTabs({
|
||||||
|
@ -32,7 +32,7 @@ export function ServiceTabs({
|
||||||
value={serviceTypeOptions[index].value}
|
value={serviceTypeOptions[index].value}
|
||||||
checked={selectedServiceType === serviceTypeOptions[index].value}
|
checked={selectedServiceType === serviceTypeOptions[index].value}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setSelectedServiceType(e.target.value as ServiceTypeValue)
|
setSelectedServiceType(e.target.value as ServiceType)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{label}
|
{label}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { SchemaOf, array, object, boolean, string, mixed, number } from 'yup';
|
import { SchemaOf, array, object, boolean, string, mixed, number } from 'yup';
|
||||||
|
|
||||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
|
||||||
|
|
||||||
import { ServiceFormValues, ServicePort } from './types';
|
import { ServiceFormValues, ServicePort } from './types';
|
||||||
import { prependWithSlash } from './utils';
|
import { prependWithSlash } from './utils';
|
||||||
|
|
||||||
|
@ -46,11 +44,7 @@ export function kubeServicesValidation(
|
||||||
Namespace: string(),
|
Namespace: string(),
|
||||||
Name: string(),
|
Name: string(),
|
||||||
StackName: string(),
|
StackName: string(),
|
||||||
Type: mixed().oneOf([
|
Type: mixed().oneOf(['ClusterIP', 'NodePort', 'LoadBalancer']),
|
||||||
KubernetesApplicationPublishingTypes.CLUSTER_IP,
|
|
||||||
KubernetesApplicationPublishingTypes.NODE_PORT,
|
|
||||||
KubernetesApplicationPublishingTypes.LOAD_BALANCER,
|
|
||||||
]),
|
|
||||||
ClusterIP: string(),
|
ClusterIP: string(),
|
||||||
ApplicationName: string(),
|
ApplicationName: string(),
|
||||||
ApplicationOwner: string(),
|
ApplicationOwner: string(),
|
||||||
|
@ -61,6 +55,7 @@ export function kubeServicesValidation(
|
||||||
object({
|
object({
|
||||||
port: number()
|
port: number()
|
||||||
.required('Service port number is required.')
|
.required('Service port number is required.')
|
||||||
|
.typeError('Service port number is required.')
|
||||||
.min(1, 'Service port number must be inside the range 1-65535.')
|
.min(1, 'Service port number must be inside the range 1-65535.')
|
||||||
.max(65535, 'Service port number must be inside the range 1-65535.')
|
.max(65535, 'Service port number must be inside the range 1-65535.')
|
||||||
.test(
|
.test(
|
||||||
|
@ -93,6 +88,7 @@ export function kubeServicesValidation(
|
||||||
),
|
),
|
||||||
targetPort: number()
|
targetPort: number()
|
||||||
.required('Container port number is required.')
|
.required('Container port number is required.')
|
||||||
|
.typeError('Container port number is required.')
|
||||||
.min(1, 'Container port number must be inside the range 1-65535.')
|
.min(1, 'Container port number must be inside the range 1-65535.')
|
||||||
.max(
|
.max(
|
||||||
65535,
|
65535,
|
||||||
|
@ -116,8 +112,7 @@ export function kubeServicesValidation(
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
matchingService === undefined ||
|
matchingService === undefined ||
|
||||||
matchingService.Type !==
|
matchingService.Type !== 'NodePort'
|
||||||
KubernetesApplicationPublishingTypes.NODE_PORT // ignore validation unless the service is of type nodeport
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -143,8 +138,7 @@ export function kubeServicesValidation(
|
||||||
|
|
||||||
if (
|
if (
|
||||||
matchingService === undefined ||
|
matchingService === undefined ||
|
||||||
matchingService.Type !==
|
matchingService.Type !== 'NodePort'
|
||||||
KubernetesApplicationPublishingTypes.NODE_PORT // ignore validation unless the service is of type nodeport
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -163,8 +157,7 @@ export function kubeServicesValidation(
|
||||||
const formNodePortsWithoutCurrentService = formServices
|
const formNodePortsWithoutCurrentService = formServices
|
||||||
.filter(
|
.filter(
|
||||||
(formService) =>
|
(formService) =>
|
||||||
formService.Type ===
|
formService.Type === 'NodePort' &&
|
||||||
KubernetesApplicationPublishingTypes.NODE_PORT &&
|
|
||||||
formService.Name !== matchingService.Name
|
formService.Name !== matchingService.Name
|
||||||
)
|
)
|
||||||
.flatMap((formService) => formService.Ports)
|
.flatMap((formService) => formService.Ports)
|
||||||
|
@ -187,11 +180,7 @@ export function kubeServicesValidation(
|
||||||
context.parent as ServicePort,
|
context.parent as ServicePort,
|
||||||
formServices
|
formServices
|
||||||
);
|
);
|
||||||
if (
|
if (!matchingService || matchingService.Type !== 'NodePort') {
|
||||||
!matchingService ||
|
|
||||||
matchingService.Type !==
|
|
||||||
KubernetesApplicationPublishingTypes.NODE_PORT
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return nodePort >= 30000;
|
return nodePort >= 30000;
|
||||||
|
@ -209,11 +198,7 @@ export function kubeServicesValidation(
|
||||||
context.parent as ServicePort,
|
context.parent as ServicePort,
|
||||||
formServices
|
formServices
|
||||||
);
|
);
|
||||||
if (
|
if (!matchingService || matchingService.Type !== 'NodePort') {
|
||||||
!matchingService ||
|
|
||||||
matchingService.Type !==
|
|
||||||
KubernetesApplicationPublishingTypes.NODE_PORT
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return nodePort <= 32767;
|
return nodePort <= 32767;
|
||||||
|
|
|
@ -93,10 +93,7 @@ export function LoadBalancerServiceForm({
|
||||||
value={servicePort.targetPort}
|
value={servicePort.targetPort}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const newServicePorts = [...servicePorts];
|
const newServicePorts = [...servicePorts];
|
||||||
const newValue =
|
const newValue = e.target.valueAsNumber;
|
||||||
e.target.value === ''
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value);
|
|
||||||
newServicePorts[portIndex] = {
|
newServicePorts[portIndex] = {
|
||||||
...newServicePorts[portIndex],
|
...newServicePorts[portIndex],
|
||||||
targetPort: newValue,
|
targetPort: newValue,
|
||||||
|
@ -119,10 +116,7 @@ export function LoadBalancerServiceForm({
|
||||||
const newServicePorts = [...servicePorts];
|
const newServicePorts = [...servicePorts];
|
||||||
newServicePorts[portIndex] = {
|
newServicePorts[portIndex] = {
|
||||||
...newServicePorts[portIndex],
|
...newServicePorts[portIndex],
|
||||||
port:
|
port: e.target.valueAsNumber,
|
||||||
e.target.value === ''
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value),
|
|
||||||
};
|
};
|
||||||
onChangePort(newServicePorts);
|
onChangePort(newServicePorts);
|
||||||
}}
|
}}
|
||||||
|
@ -140,7 +134,7 @@ export function LoadBalancerServiceForm({
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control min-w-max"
|
className="form-control min-w-max"
|
||||||
name={`loadbalancer_port_${portIndex}`}
|
name={`loadbalancer_port_${portIndex}`}
|
||||||
placeholder="80"
|
placeholder="e.g. 80"
|
||||||
min="1"
|
min="1"
|
||||||
max="65535"
|
max="65535"
|
||||||
value={servicePort.port || ''}
|
value={servicePort.port || ''}
|
||||||
|
@ -148,10 +142,7 @@ export function LoadBalancerServiceForm({
|
||||||
const newServicePorts = [...servicePorts];
|
const newServicePorts = [...servicePorts];
|
||||||
newServicePorts[portIndex] = {
|
newServicePorts[portIndex] = {
|
||||||
...newServicePorts[portIndex],
|
...newServicePorts[portIndex],
|
||||||
port:
|
port: e.target.valueAsNumber,
|
||||||
e.target.value === ''
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value),
|
|
||||||
};
|
};
|
||||||
onChangePort(newServicePorts);
|
onChangePort(newServicePorts);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Plus, RefreshCw } from 'lucide-react';
|
import { Plus, RefreshCw } from 'lucide-react';
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
|
|
||||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
|
||||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
import { useEnvironment } from '@/react/portainer/environments/queries';
|
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
@ -49,8 +48,7 @@ export function LoadBalancerServicesForm({
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadBalancerServiceCount = services.filter(
|
const loadBalancerServiceCount = services.filter(
|
||||||
(service) =>
|
(service) => service.Type === 'LoadBalancer'
|
||||||
service.Type === KubernetesApplicationPublishingTypes.LOAD_BALANCER
|
|
||||||
).length;
|
).length;
|
||||||
return (
|
return (
|
||||||
<Card className="pb-5">
|
<Card className="pb-5">
|
||||||
|
@ -95,8 +93,7 @@ export function LoadBalancerServicesForm({
|
||||||
{loadBalancerServiceCount > 0 && (
|
{loadBalancerServiceCount > 0 && (
|
||||||
<div className="flex w-full flex-col gap-4">
|
<div className="flex w-full flex-col gap-4">
|
||||||
{services.map((service, index) =>
|
{services.map((service, index) =>
|
||||||
service.Type ===
|
service.Type === 'LoadBalancer' ? (
|
||||||
KubernetesApplicationPublishingTypes.LOAD_BALANCER ? (
|
|
||||||
<LoadBalancerServiceForm
|
<LoadBalancerServiceForm
|
||||||
key={index}
|
key={index}
|
||||||
serviceName={service.Name}
|
serviceName={service.Name}
|
||||||
|
@ -131,8 +128,7 @@ export function LoadBalancerServicesForm({
|
||||||
services.length + 1,
|
services.length + 1,
|
||||||
services
|
services
|
||||||
);
|
);
|
||||||
newService.Type =
|
newService.Type = 'LoadBalancer';
|
||||||
KubernetesApplicationPublishingTypes.LOAD_BALANCER;
|
|
||||||
const newServicePort = newPort(newService.Name);
|
const newServicePort = newPort(newService.Name);
|
||||||
newService.Ports = [newServicePort];
|
newService.Ports = [newServicePort];
|
||||||
newService.Selector = selector;
|
newService.Selector = selector;
|
||||||
|
|
|
@ -94,10 +94,7 @@ export function NodePortServiceForm({
|
||||||
value={servicePort.targetPort}
|
value={servicePort.targetPort}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const newServicePorts = [...servicePorts];
|
const newServicePorts = [...servicePorts];
|
||||||
const newValue =
|
const newValue = e.target.valueAsNumber;
|
||||||
e.target.value === ''
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value);
|
|
||||||
newServicePorts[portIndex] = {
|
newServicePorts[portIndex] = {
|
||||||
...newServicePorts[portIndex],
|
...newServicePorts[portIndex],
|
||||||
targetPort: newValue,
|
targetPort: newValue,
|
||||||
|
@ -120,10 +117,7 @@ export function NodePortServiceForm({
|
||||||
const newServicePorts = [...servicePorts];
|
const newServicePorts = [...servicePorts];
|
||||||
newServicePorts[portIndex] = {
|
newServicePorts[portIndex] = {
|
||||||
...newServicePorts[portIndex],
|
...newServicePorts[portIndex],
|
||||||
port:
|
port: e.target.valueAsNumber,
|
||||||
e.target.value === ''
|
|
||||||
? undefined
|
|
||||||
: Number(e.target.value),
|
|
||||||
};
|
};
|
||||||
onChangePort(newServicePorts);
|
onChangePort(newServicePorts);
|
||||||
}}
|
}}
|
||||||
|
@ -139,7 +133,7 @@ export function NodePortServiceForm({
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control min-w-max"
|
className="form-control min-w-max"
|
||||||
name={`node_port_${portIndex}`}
|
name={`node_port_${portIndex}`}
|
||||||
placeholder="30080"
|
placeholder="e.g. 30080"
|
||||||
min="30000"
|
min="30000"
|
||||||
max="32767"
|
max="32767"
|
||||||
value={servicePort.nodePort ?? ''}
|
value={servicePort.nodePort ?? ''}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
|
|
||||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
|
||||||
|
|
||||||
import { Card } from '@@/Card';
|
import { Card } from '@@/Card';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
|
@ -36,7 +34,7 @@ export function NodePortServicesForm({
|
||||||
isEditMode,
|
isEditMode,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const nodePortServiceCount = services.filter(
|
const nodePortServiceCount = services.filter(
|
||||||
(service) => service.Type === KubernetesApplicationPublishingTypes.NODE_PORT
|
(service) => service.Type === 'NodePort'
|
||||||
).length;
|
).length;
|
||||||
return (
|
return (
|
||||||
<Card className="pb-5">
|
<Card className="pb-5">
|
||||||
|
@ -48,8 +46,7 @@ export function NodePortServicesForm({
|
||||||
{nodePortServiceCount > 0 && (
|
{nodePortServiceCount > 0 && (
|
||||||
<div className="flex w-full flex-col gap-4">
|
<div className="flex w-full flex-col gap-4">
|
||||||
{services.map((service, index) =>
|
{services.map((service, index) =>
|
||||||
service.Type ===
|
service.Type === 'NodePort' ? (
|
||||||
KubernetesApplicationPublishingTypes.NODE_PORT ? (
|
|
||||||
<NodePortServiceForm
|
<NodePortServiceForm
|
||||||
key={index}
|
key={index}
|
||||||
serviceName={service.Name}
|
serviceName={service.Name}
|
||||||
|
@ -84,7 +81,7 @@ export function NodePortServicesForm({
|
||||||
services.length + 1,
|
services.length + 1,
|
||||||
services
|
services
|
||||||
);
|
);
|
||||||
newService.Type = KubernetesApplicationPublishingTypes.NODE_PORT;
|
newService.Type = 'NodePort';
|
||||||
const newServicePort = newPort(newService.Name);
|
const newServicePort = newPort(newService.Name);
|
||||||
newService.Ports = [newServicePort];
|
newService.Ports = [newServicePort];
|
||||||
newService.Selector = selector;
|
newService.Selector = selector;
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
|
||||||
|
|
||||||
export interface ServicePort {
|
export interface ServicePort {
|
||||||
port?: number;
|
port: number;
|
||||||
targetPort?: number;
|
targetPort?: number;
|
||||||
nodePort?: number;
|
nodePort?: number;
|
||||||
serviceName?: string;
|
serviceName?: string;
|
||||||
|
@ -12,9 +10,6 @@ export interface ServicePort {
|
||||||
ingressPaths?: ServicePortIngressPath[];
|
ingressPaths?: ServicePortIngressPath[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceTypeAngularEnum =
|
|
||||||
(typeof KubernetesApplicationPublishingTypes)[keyof typeof KubernetesApplicationPublishingTypes];
|
|
||||||
|
|
||||||
export type ServicePortIngressPath = {
|
export type ServicePortIngressPath = {
|
||||||
IngressName?: string;
|
IngressName?: string;
|
||||||
Host?: string;
|
Host?: string;
|
||||||
|
@ -24,7 +19,7 @@ export type ServicePortIngressPath = {
|
||||||
export type ServiceFormValues = {
|
export type ServiceFormValues = {
|
||||||
Headless: boolean;
|
Headless: boolean;
|
||||||
Ports: ServicePort[];
|
Ports: ServicePort[];
|
||||||
Type: ServiceTypeAngularEnum;
|
Type: ServiceType;
|
||||||
Ingress: boolean;
|
Ingress: boolean;
|
||||||
ClusterIP?: string;
|
ClusterIP?: string;
|
||||||
ApplicationName?: string;
|
ApplicationName?: string;
|
||||||
|
@ -36,9 +31,9 @@ export type ServiceFormValues = {
|
||||||
Namespace?: string;
|
Namespace?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServiceTypeValue = 'ClusterIP' | 'NodePort' | 'LoadBalancer';
|
export type ServiceType = 'ClusterIP' | 'NodePort' | 'LoadBalancer';
|
||||||
export type ServiceTypeOption = {
|
export type ServiceTypeOption = {
|
||||||
value: ServiceTypeValue;
|
value: ServiceType;
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
import { Ingress } from '@/react/kubernetes/ingresses/types';
|
import { compare } from 'fast-json-patch';
|
||||||
|
import { Service, ServiceSpec } from 'kubernetes-types/core/v1';
|
||||||
|
import { ObjectMeta } from 'kubernetes-types/meta/v1';
|
||||||
|
import angular from 'angular';
|
||||||
|
|
||||||
|
import { Ingress as IngressFormValues } from '@/react/kubernetes/ingresses/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
appNameLabel,
|
||||||
|
appOwnerLabel,
|
||||||
|
appStackNameLabel,
|
||||||
|
} from '../../constants';
|
||||||
|
|
||||||
import { ServiceFormValues, ServicePort } from './types';
|
import { ServiceFormValues, ServicePort } from './types';
|
||||||
|
|
||||||
export function newPort(serviceName?: string) {
|
export function newPort(serviceName?: string): ServicePort {
|
||||||
return {
|
return {
|
||||||
port: undefined,
|
port: 80,
|
||||||
targetPort: undefined,
|
targetPort: undefined,
|
||||||
name: '',
|
name: '',
|
||||||
protocol: 'TCP',
|
protocol: 'TCP',
|
||||||
|
@ -43,7 +54,7 @@ export const serviceFormDefaultValues: ServiceFormValues = {
|
||||||
Name: '',
|
Name: '',
|
||||||
StackName: '',
|
StackName: '',
|
||||||
Ports: [],
|
Ports: [],
|
||||||
Type: 1, // clusterip type as default
|
Type: 'ClusterIP',
|
||||||
ClusterIP: '',
|
ClusterIP: '',
|
||||||
ApplicationName: '',
|
ApplicationName: '',
|
||||||
ApplicationOwner: '',
|
ApplicationOwner: '',
|
||||||
|
@ -54,16 +65,16 @@ export const serviceFormDefaultValues: ServiceFormValues = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates new Ingress objects from form path data
|
* Generates new Ingress objects from form path data
|
||||||
* @param {Ingress[]} oldIngresses - The old Ingress objects
|
* @param {IngressFormValues[]} oldIngresses - The old Ingress objects
|
||||||
* @param {ServicePort[]} newServicesPorts - The new ServicePort objects from the form
|
* @param {ServicePort[]} newServicesPorts - The new ServicePort objects from the form
|
||||||
* @param {ServicePort[]} oldServicesPorts - The old ServicePort objects
|
* @param {ServicePort[]} oldServicesPorts - The old ServicePort objects
|
||||||
* @returns {Ingress[]} The new Ingress objects
|
* @returns {IngressFormValues[]} The new Ingress objects
|
||||||
*/
|
*/
|
||||||
export function generateNewIngressesFromFormPaths(
|
export function generateNewIngressesFromFormPaths(
|
||||||
oldIngresses?: Ingress[],
|
oldIngresses?: IngressFormValues[],
|
||||||
newServicesPorts?: ServicePort[],
|
newServicesPorts?: ServicePort[],
|
||||||
oldServicesPorts?: ServicePort[]
|
oldServicesPorts?: ServicePort[]
|
||||||
): Ingress[] {
|
): IngressFormValues[] {
|
||||||
// filter the ports to only the ones that have an ingress
|
// filter the ports to only the ones that have an ingress
|
||||||
const oldIngressPaths = oldServicesPorts
|
const oldIngressPaths = oldServicesPorts
|
||||||
?.flatMap((port) => port.ingressPaths)
|
?.flatMap((port) => port.ingressPaths)
|
||||||
|
@ -77,7 +88,7 @@ export function generateNewIngressesFromFormPaths(
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the old paths from the newIngresses copy
|
// remove the old paths from the newIngresses copy
|
||||||
const newIngresses = structuredClone(oldIngresses) ?? [];
|
const newIngresses: IngressFormValues[] = angular.copy(oldIngresses) ?? []; // the current jest version doesn't support structured cloning, so we need to use angular.copy
|
||||||
oldIngressPaths?.forEach((oldIngressPath) => {
|
oldIngressPaths?.forEach((oldIngressPath) => {
|
||||||
if (!oldIngressPath?.Path) return;
|
if (!oldIngressPath?.Path) return;
|
||||||
const newMatchingIng = newIngresses?.find(
|
const newMatchingIng = newIngresses?.find(
|
||||||
|
@ -151,3 +162,57 @@ export function prependWithSlash(path?: string) {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
return path.startsWith('/') ? path : `/${path}`;
|
return path.startsWith('/') ? path : `/${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getServicePatchPayload(
|
||||||
|
oldService: ServiceFormValues,
|
||||||
|
newService: ServiceFormValues
|
||||||
|
) {
|
||||||
|
const oldPayload = getServicePayload(oldService);
|
||||||
|
const newPayload = getServicePayload(newService);
|
||||||
|
|
||||||
|
const payload = compare(oldPayload, newPayload);
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServicePayload(service: ServiceFormValues): Service {
|
||||||
|
if (!service.Name || !service.Namespace) {
|
||||||
|
throw new Error('Service name and namespace are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// metadata
|
||||||
|
const labels: Record<string, string> = {};
|
||||||
|
if (service.ApplicationName) {
|
||||||
|
labels[appNameLabel] = service.ApplicationName;
|
||||||
|
}
|
||||||
|
if (service.ApplicationOwner) {
|
||||||
|
labels[appOwnerLabel] = service.ApplicationOwner;
|
||||||
|
}
|
||||||
|
if (service.StackName) {
|
||||||
|
labels[appStackNameLabel] = service.StackName;
|
||||||
|
}
|
||||||
|
const metadata: ObjectMeta = {
|
||||||
|
name: service.Name,
|
||||||
|
namespace: service.Namespace,
|
||||||
|
labels,
|
||||||
|
};
|
||||||
|
|
||||||
|
// spec
|
||||||
|
const ports = service.Headless ? [] : service.Ports;
|
||||||
|
const selector = service.Selector;
|
||||||
|
const clusterIP = service.Headless ? 'None' : service.ClusterIP;
|
||||||
|
const type = service.Headless ? 'ClusterIP' : service.Type;
|
||||||
|
const spec: ServiceSpec = {
|
||||||
|
ports,
|
||||||
|
selector,
|
||||||
|
clusterIP,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
|
||||||
|
const servicePayload: Service = {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Service',
|
||||||
|
metadata,
|
||||||
|
spec,
|
||||||
|
};
|
||||||
|
return servicePayload;
|
||||||
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { Boxes, Sliders } from 'lucide-react';
|
import { Boxes, Sliders } from 'lucide-react';
|
||||||
|
|
||||||
import { KubernetesApplicationDeploymentTypes } from '@/kubernetes/models/application/models';
|
|
||||||
|
|
||||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
import { BoxSelectorOption } from '@@/BoxSelector';
|
||||||
|
|
||||||
|
import { DeploymentType } from '../types';
|
||||||
|
|
||||||
export function getDeploymentOptions(
|
export function getDeploymentOptions(
|
||||||
supportGlobalDeployment: boolean
|
supportGlobalDeployment: boolean
|
||||||
): ReadonlyArray<BoxSelectorOption<number>> {
|
): ReadonlyArray<BoxSelectorOption<DeploymentType>> {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'deployment_replicated',
|
id: 'deployment_replicated',
|
||||||
label: 'Replicated',
|
label: 'Replicated',
|
||||||
value: KubernetesApplicationDeploymentTypes.REPLICATED,
|
value: 'Replicated',
|
||||||
icon: Sliders,
|
icon: Sliders,
|
||||||
iconType: 'badge',
|
iconType: 'badge',
|
||||||
description: 'Run one or multiple instances of this container',
|
description: 'Run one or multiple instances of this container',
|
||||||
|
@ -26,7 +26,7 @@ export function getDeploymentOptions(
|
||||||
label: 'Global',
|
label: 'Global',
|
||||||
description:
|
description:
|
||||||
'Application will be deployed as a DaemonSet with an instance on each node of the cluster',
|
'Application will be deployed as a DaemonSet with an instance on each node of the cluster',
|
||||||
value: KubernetesApplicationDeploymentTypes.GLOBAL,
|
value: 'Global',
|
||||||
icon: Boxes,
|
icon: Boxes,
|
||||||
iconType: 'badge',
|
iconType: 'badge',
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
|
import { ApplicationFormValues } from '../../types';
|
||||||
|
|
||||||
|
import { getAppResourceSummaries, getArticle } from './utils';
|
||||||
|
import { Summary } from './types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
formValues: ApplicationFormValues;
|
||||||
|
oldFormValues: ApplicationFormValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ApplicationSummarySection({
|
||||||
|
formValues,
|
||||||
|
oldFormValues,
|
||||||
|
}: Props) {
|
||||||
|
// extract cpu and memory requests & limits for pod
|
||||||
|
const limits = {
|
||||||
|
cpu: formValues.CpuLimit,
|
||||||
|
memory: formValues.MemoryLimit,
|
||||||
|
};
|
||||||
|
const appResourceSummaries = getAppResourceSummaries(
|
||||||
|
formValues,
|
||||||
|
oldFormValues
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!appResourceSummaries || appResourceSummaries?.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormSection title="Summary" isFoldable defaultFolded={false}>
|
||||||
|
<TextTip color="blue">
|
||||||
|
Portainer will execute the following Kubernetes actions.
|
||||||
|
</TextTip>
|
||||||
|
<ul className="w-full small text-muted ml-5">
|
||||||
|
{appResourceSummaries.map((summary) => (
|
||||||
|
<SummaryItem key={JSON.stringify(summary)} summary={summary} />
|
||||||
|
))}
|
||||||
|
{!!limits.memory && (
|
||||||
|
<li>
|
||||||
|
Set the memory resources limits and requests to{' '}
|
||||||
|
<code>{limits.memory}M</code>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{!!limits.cpu && (
|
||||||
|
<li>
|
||||||
|
Set the CPU resources limits and requests to{' '}
|
||||||
|
<code>{limits.cpu}</code>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SummaryItem({ summary }: { summary: Summary }) {
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
{`${summary.action} ${getArticle(summary.kind, summary.action)} `}
|
||||||
|
<span className="bold">{summary.kind}</span>
|
||||||
|
{' named '}
|
||||||
|
<code>{summary.name}</code>
|
||||||
|
{!!summary.type && (
|
||||||
|
<span>
|
||||||
|
{' of type '}
|
||||||
|
<code>{summary.type}</code>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { ApplicationSummarySection } from './ApplicationSummarySection';
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { AppKind } from '../../types';
|
||||||
|
|
||||||
|
export type KubernetesResourceAction = 'Create' | 'Update' | 'Delete';
|
||||||
|
|
||||||
|
export type KubernetesResourceType =
|
||||||
|
| AppKind
|
||||||
|
| 'Namespace'
|
||||||
|
| 'ResourceQuota'
|
||||||
|
| 'ConfigMap'
|
||||||
|
| 'Secret'
|
||||||
|
| 'PersistentVolumeClaim'
|
||||||
|
| 'Service'
|
||||||
|
| 'Ingress'
|
||||||
|
| 'HorizontalPodAutoscaler';
|
||||||
|
|
||||||
|
export type Summary = {
|
||||||
|
action: KubernetesResourceAction;
|
||||||
|
kind: KubernetesResourceType;
|
||||||
|
name: string;
|
||||||
|
type?: string;
|
||||||
|
};
|
|
@ -0,0 +1,517 @@
|
||||||
|
import { ApplicationFormValues } from '../../types';
|
||||||
|
|
||||||
|
import { Summary } from './types';
|
||||||
|
import { getAppResourceSummaries } from './utils';
|
||||||
|
|
||||||
|
const complicatedStatefulSet: ApplicationFormValues = {
|
||||||
|
ApplicationType: 'StatefulSet',
|
||||||
|
ResourcePool: {
|
||||||
|
Namespace: {
|
||||||
|
Id: '9ef75267-3cf4-46f6-879a-5baeceb5c477',
|
||||||
|
Name: 'default',
|
||||||
|
CreationDate: '2023-08-30T18:55:34Z',
|
||||||
|
Status: 'Active',
|
||||||
|
Yaml: '',
|
||||||
|
IsSystem: false,
|
||||||
|
Annotations: [],
|
||||||
|
},
|
||||||
|
Ingresses: [],
|
||||||
|
Yaml: '',
|
||||||
|
$$hashKey: 'object:702',
|
||||||
|
},
|
||||||
|
Name: 'my-app',
|
||||||
|
StackName: '',
|
||||||
|
ApplicationOwner: '',
|
||||||
|
ImageModel: {
|
||||||
|
UseRegistry: true,
|
||||||
|
Registry: {
|
||||||
|
Id: 0,
|
||||||
|
Type: 0,
|
||||||
|
Name: 'Docker Hub (anonymous)',
|
||||||
|
URL: 'docker.io',
|
||||||
|
},
|
||||||
|
Image: 'caddy',
|
||||||
|
},
|
||||||
|
Note: '',
|
||||||
|
MemoryLimit: 512,
|
||||||
|
CpuLimit: 0.5,
|
||||||
|
DeploymentType: 'Replicated',
|
||||||
|
ReplicaCount: 1,
|
||||||
|
AutoScaler: {
|
||||||
|
isUsed: true,
|
||||||
|
minReplicas: 1,
|
||||||
|
maxReplicas: 3,
|
||||||
|
targetCpuUtilizationPercentage: 50,
|
||||||
|
},
|
||||||
|
Containers: [],
|
||||||
|
Services: [
|
||||||
|
{
|
||||||
|
Headless: false,
|
||||||
|
Namespace: '',
|
||||||
|
Name: 'my-app',
|
||||||
|
StackName: '',
|
||||||
|
Ports: [
|
||||||
|
{
|
||||||
|
port: 80,
|
||||||
|
targetPort: 80,
|
||||||
|
name: '',
|
||||||
|
protocol: 'TCP',
|
||||||
|
serviceName: 'my-app',
|
||||||
|
ingressPaths: [
|
||||||
|
{
|
||||||
|
Host: '127.0.0.1.nip.io',
|
||||||
|
IngressName: 'default-ingress-3',
|
||||||
|
Path: '/test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Type: 'ClusterIP',
|
||||||
|
ClusterIP: '',
|
||||||
|
ApplicationName: '',
|
||||||
|
ApplicationOwner: '',
|
||||||
|
Note: '',
|
||||||
|
Ingress: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Headless: false,
|
||||||
|
Namespace: '',
|
||||||
|
Name: 'my-app-2',
|
||||||
|
StackName: '',
|
||||||
|
Ports: [
|
||||||
|
{
|
||||||
|
port: 80,
|
||||||
|
targetPort: 80,
|
||||||
|
name: '',
|
||||||
|
protocol: 'TCP',
|
||||||
|
nodePort: 30080,
|
||||||
|
serviceName: 'my-app-2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Type: 'NodePort',
|
||||||
|
ClusterIP: '',
|
||||||
|
ApplicationName: '',
|
||||||
|
ApplicationOwner: '',
|
||||||
|
Note: '',
|
||||||
|
Ingress: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Headless: false,
|
||||||
|
Namespace: '',
|
||||||
|
Name: 'my-app-3',
|
||||||
|
StackName: '',
|
||||||
|
Ports: [
|
||||||
|
{
|
||||||
|
port: 80,
|
||||||
|
targetPort: 80,
|
||||||
|
name: '',
|
||||||
|
protocol: 'TCP',
|
||||||
|
serviceName: 'my-app-3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Type: 'LoadBalancer',
|
||||||
|
ClusterIP: '',
|
||||||
|
ApplicationName: '',
|
||||||
|
ApplicationOwner: '',
|
||||||
|
Note: '',
|
||||||
|
Ingress: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
EnvironmentVariables: [],
|
||||||
|
DataAccessPolicy: 'Isolated',
|
||||||
|
PersistedFolders: [
|
||||||
|
{
|
||||||
|
persistentVolumeClaimName: 'my-app-6be07c40-de3a-4775-a29b-19a60890052e',
|
||||||
|
containerPath: 'test',
|
||||||
|
size: '1',
|
||||||
|
sizeUnit: 'GB',
|
||||||
|
storageClass: {
|
||||||
|
Name: 'local-path',
|
||||||
|
AccessModes: ['RWO', 'RWX'],
|
||||||
|
Provisioner: 'rancher.io/local-path',
|
||||||
|
AllowVolumeExpansion: true,
|
||||||
|
},
|
||||||
|
useNewVolume: true,
|
||||||
|
needsDeletion: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ConfigMaps: [],
|
||||||
|
Secrets: [],
|
||||||
|
PlacementType: 'preferred',
|
||||||
|
Placements: [],
|
||||||
|
Annotations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const complicatedStatefulSetNoServices: ApplicationFormValues = {
|
||||||
|
ApplicationType: 'StatefulSet',
|
||||||
|
ResourcePool: {
|
||||||
|
Namespace: {
|
||||||
|
Id: '9ef75267-3cf4-46f6-879a-5baeceb5c477',
|
||||||
|
Name: 'default',
|
||||||
|
CreationDate: '2023-08-30T18:55:34Z',
|
||||||
|
Status: 'Active',
|
||||||
|
Yaml: '',
|
||||||
|
IsSystem: false,
|
||||||
|
Annotations: [],
|
||||||
|
},
|
||||||
|
Ingresses: [],
|
||||||
|
Yaml: '',
|
||||||
|
$$hashKey: 'object:129',
|
||||||
|
},
|
||||||
|
Name: 'my-app',
|
||||||
|
StackName: 'my-app',
|
||||||
|
ApplicationOwner: 'admin',
|
||||||
|
ImageModel: {
|
||||||
|
UseRegistry: true,
|
||||||
|
Registry: {
|
||||||
|
Id: 0,
|
||||||
|
Type: 0,
|
||||||
|
Name: 'Docker Hub (anonymous)',
|
||||||
|
URL: 'docker.io',
|
||||||
|
},
|
||||||
|
Image: 'caddy:latest',
|
||||||
|
},
|
||||||
|
Note: '',
|
||||||
|
MemoryLimit: 512,
|
||||||
|
CpuLimit: 0.5,
|
||||||
|
DeploymentType: 'Replicated',
|
||||||
|
ReplicaCount: 1,
|
||||||
|
AutoScaler: {
|
||||||
|
minReplicas: 1,
|
||||||
|
maxReplicas: 3,
|
||||||
|
targetCpuUtilizationPercentage: 50,
|
||||||
|
isUsed: true,
|
||||||
|
},
|
||||||
|
Containers: [
|
||||||
|
{
|
||||||
|
Type: 2,
|
||||||
|
PodName: 'my-app-0',
|
||||||
|
Name: 'my-app',
|
||||||
|
Image: 'caddy:latest',
|
||||||
|
ImagePullPolicy: 'Always',
|
||||||
|
Status: 'Terminated',
|
||||||
|
Limits: {
|
||||||
|
cpu: '500m',
|
||||||
|
memory: '512M',
|
||||||
|
},
|
||||||
|
Requests: {
|
||||||
|
cpu: '500m',
|
||||||
|
memory: '512M',
|
||||||
|
},
|
||||||
|
VolumeMounts: [
|
||||||
|
{
|
||||||
|
name: 'test-6be07c40-de3a-4775-a29b-19a60890052e-test-0',
|
||||||
|
mountPath: '/test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'kube-api-access-n4vht',
|
||||||
|
readOnly: true,
|
||||||
|
mountPath: '/var/run/secrets/kubernetes.io/serviceaccount',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ConfigurationVolumes: [],
|
||||||
|
PersistedFolders: [
|
||||||
|
{
|
||||||
|
MountPath: '/test',
|
||||||
|
persistentVolumeClaimName:
|
||||||
|
'test-6be07c40-de3a-4775-a29b-19a60890052e-test-0',
|
||||||
|
HostPath: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Services: [],
|
||||||
|
EnvironmentVariables: [],
|
||||||
|
DataAccessPolicy: 'Isolated',
|
||||||
|
PersistedFolders: [
|
||||||
|
{
|
||||||
|
persistentVolumeClaimName:
|
||||||
|
'test-6be07c40-de3a-4775-a29b-19a60890052e-test-0',
|
||||||
|
needsDeletion: false,
|
||||||
|
containerPath: '/test',
|
||||||
|
size: '1',
|
||||||
|
sizeUnit: 'GB',
|
||||||
|
storageClass: {
|
||||||
|
Name: 'local-path',
|
||||||
|
AccessModes: ['RWO', 'RWX'],
|
||||||
|
Provisioner: 'rancher.io/local-path',
|
||||||
|
AllowVolumeExpansion: true,
|
||||||
|
},
|
||||||
|
useNewVolume: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ConfigMaps: [],
|
||||||
|
Secrets: [],
|
||||||
|
PlacementType: 'preferred',
|
||||||
|
Placements: [],
|
||||||
|
Annotations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createComplicatedStatefulSetSummaries: Array<Summary> = [
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'StatefulSet',
|
||||||
|
name: 'my-app',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'Service',
|
||||||
|
name: 'my-app',
|
||||||
|
type: 'ClusterIP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'Service',
|
||||||
|
name: 'my-app-2',
|
||||||
|
type: 'NodePort',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'Service',
|
||||||
|
name: 'my-app-3',
|
||||||
|
type: 'LoadBalancer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'Service',
|
||||||
|
name: 'headless-my-app',
|
||||||
|
type: 'ClusterIP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Update',
|
||||||
|
kind: 'Ingress',
|
||||||
|
name: 'default-ingress-3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'HorizontalPodAutoscaler',
|
||||||
|
name: 'my-app',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const simpleDaemonset: ApplicationFormValues = {
|
||||||
|
ApplicationType: 'DaemonSet',
|
||||||
|
ResourcePool: {
|
||||||
|
Namespace: {
|
||||||
|
Id: '49acd824-0ee4-46d1-b1e2-3d36a64ce7e4',
|
||||||
|
Name: 'default',
|
||||||
|
CreationDate: '2023-12-19T06:40:12Z',
|
||||||
|
Status: 'Active',
|
||||||
|
Yaml: '',
|
||||||
|
IsSystem: false,
|
||||||
|
Annotations: [],
|
||||||
|
},
|
||||||
|
Ingresses: [],
|
||||||
|
Yaml: '',
|
||||||
|
$$hashKey: 'object:418',
|
||||||
|
},
|
||||||
|
Name: 'my-app',
|
||||||
|
StackName: '',
|
||||||
|
ApplicationOwner: '',
|
||||||
|
ImageModel: {
|
||||||
|
UseRegistry: true,
|
||||||
|
Registry: {
|
||||||
|
Id: 0,
|
||||||
|
Type: 0,
|
||||||
|
Name: 'Docker Hub (anonymous)',
|
||||||
|
URL: 'docker.io',
|
||||||
|
},
|
||||||
|
Image: 'caddy',
|
||||||
|
},
|
||||||
|
Note: '',
|
||||||
|
MemoryLimit: 0,
|
||||||
|
CpuLimit: 0,
|
||||||
|
DeploymentType: 'Global',
|
||||||
|
ReplicaCount: 1,
|
||||||
|
Containers: [],
|
||||||
|
DataAccessPolicy: 'Shared',
|
||||||
|
PersistedFolders: [
|
||||||
|
{
|
||||||
|
persistentVolumeClaimName: 'my-app-7c114420-a5d0-491c-8bd6-ec70c3d380be',
|
||||||
|
containerPath: '/test',
|
||||||
|
size: '1',
|
||||||
|
sizeUnit: 'GB',
|
||||||
|
storageClass: {
|
||||||
|
Name: 'oci',
|
||||||
|
AccessModes: ['RWO', 'RWX'],
|
||||||
|
Provisioner: 'oracle.com/oci',
|
||||||
|
AllowVolumeExpansion: true,
|
||||||
|
},
|
||||||
|
useNewVolume: true,
|
||||||
|
needsDeletion: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
PlacementType: 'preferred',
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSimpleDaemonsetSummaries: Array<Summary> = [
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'DaemonSet',
|
||||||
|
name: 'my-app',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'PersistentVolumeClaim',
|
||||||
|
name: 'my-app-7c114420-a5d0-491c-8bd6-ec70c3d380be',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const simpleDeployment: ApplicationFormValues = {
|
||||||
|
ApplicationType: 'Deployment',
|
||||||
|
ResourcePool: {
|
||||||
|
Namespace: {
|
||||||
|
Id: '49acd824-0ee4-46d1-b1e2-3d36a64ce7e4',
|
||||||
|
Name: 'default',
|
||||||
|
CreationDate: '2023-12-19T06:40:12Z',
|
||||||
|
Status: 'Active',
|
||||||
|
Yaml: '',
|
||||||
|
IsSystem: false,
|
||||||
|
Annotations: [],
|
||||||
|
},
|
||||||
|
Ingresses: [],
|
||||||
|
Yaml: '',
|
||||||
|
$$hashKey: 'object:582',
|
||||||
|
},
|
||||||
|
Name: 'my-app',
|
||||||
|
StackName: '',
|
||||||
|
ApplicationOwner: '',
|
||||||
|
ImageModel: {
|
||||||
|
UseRegistry: true,
|
||||||
|
Registry: {
|
||||||
|
Id: 0,
|
||||||
|
Type: 0,
|
||||||
|
Name: 'Docker Hub (anonymous)',
|
||||||
|
URL: 'docker.io',
|
||||||
|
},
|
||||||
|
Image: 'caddy',
|
||||||
|
},
|
||||||
|
Note: '',
|
||||||
|
MemoryLimit: 512,
|
||||||
|
CpuLimit: 0.5,
|
||||||
|
DeploymentType: 'Replicated',
|
||||||
|
ReplicaCount: 1,
|
||||||
|
Containers: [],
|
||||||
|
DataAccessPolicy: 'Isolated',
|
||||||
|
PlacementType: 'preferred',
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSimpleDeploymentSummaries: Array<Summary> = [
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'Deployment',
|
||||||
|
name: 'my-app',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('getCreateAppSummaries', () => {
|
||||||
|
const tests: {
|
||||||
|
oldFormValues?: ApplicationFormValues;
|
||||||
|
newFormValues: ApplicationFormValues;
|
||||||
|
expected: Array<Summary>;
|
||||||
|
title: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
oldFormValues: undefined,
|
||||||
|
newFormValues: complicatedStatefulSet,
|
||||||
|
expected: createComplicatedStatefulSetSummaries,
|
||||||
|
title: 'should return create summaries for a complicated statefulset',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
oldFormValues: undefined,
|
||||||
|
newFormValues: simpleDaemonset,
|
||||||
|
expected: createSimpleDaemonsetSummaries,
|
||||||
|
title: 'should return create summaries for a simple daemonset',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
oldFormValues: undefined,
|
||||||
|
newFormValues: simpleDeployment,
|
||||||
|
expected: createSimpleDeploymentSummaries,
|
||||||
|
title: 'should return create summaries for a simple deployment',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
tests.forEach((test) => {
|
||||||
|
// eslint-disable-next-line jest/valid-title
|
||||||
|
it(test.title, () => {
|
||||||
|
expect(
|
||||||
|
getAppResourceSummaries(test.newFormValues, test.oldFormValues)
|
||||||
|
).toEqual(test.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateComplicatedStatefulSetSummaries: Array<Summary> = [
|
||||||
|
{
|
||||||
|
action: 'Update',
|
||||||
|
kind: 'StatefulSet',
|
||||||
|
name: 'my-app',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Delete',
|
||||||
|
kind: 'Service',
|
||||||
|
name: 'my-app',
|
||||||
|
type: 'ClusterIP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Delete',
|
||||||
|
kind: 'Service',
|
||||||
|
name: 'my-app-2',
|
||||||
|
type: 'NodePort',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Delete',
|
||||||
|
kind: 'Service',
|
||||||
|
name: 'my-app-3',
|
||||||
|
type: 'LoadBalancer',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const updateDeploymentToStatefulSetSummaries: Array<Summary> = [
|
||||||
|
{
|
||||||
|
action: 'Delete',
|
||||||
|
kind: 'Deployment',
|
||||||
|
name: 'my-app',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'StatefulSet',
|
||||||
|
name: 'my-app',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'HorizontalPodAutoscaler',
|
||||||
|
name: 'my-app',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('getUpdateAppSummaries', () => {
|
||||||
|
const tests: {
|
||||||
|
oldFormValues: ApplicationFormValues;
|
||||||
|
newFormValues: ApplicationFormValues;
|
||||||
|
expected: Array<Summary>;
|
||||||
|
title: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
oldFormValues: complicatedStatefulSet,
|
||||||
|
newFormValues: complicatedStatefulSetNoServices,
|
||||||
|
expected: updateComplicatedStatefulSetSummaries,
|
||||||
|
title:
|
||||||
|
'should return update summaries for removing services from statefulset',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
oldFormValues: simpleDeployment,
|
||||||
|
newFormValues: complicatedStatefulSetNoServices,
|
||||||
|
expected: updateDeploymentToStatefulSetSummaries,
|
||||||
|
title:
|
||||||
|
'should return update summaries for changing deployment to statefulset',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
tests.forEach((test) => {
|
||||||
|
// eslint-disable-next-line jest/valid-title
|
||||||
|
it(test.title, () => {
|
||||||
|
expect(
|
||||||
|
getAppResourceSummaries(test.newFormValues, test.oldFormValues)
|
||||||
|
).toEqual(test.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,362 @@
|
||||||
|
import { Ingress } from '@/react/kubernetes/ingresses/types';
|
||||||
|
|
||||||
|
import { ServiceFormValues } from '../../CreateView/application-services/types';
|
||||||
|
import { ApplicationFormValues } from '../../types';
|
||||||
|
import {
|
||||||
|
generateNewIngressesFromFormPaths,
|
||||||
|
getServicePatchPayload,
|
||||||
|
} from '../../CreateView/application-services/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
KubernetesResourceType,
|
||||||
|
KubernetesResourceAction,
|
||||||
|
Summary,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export function getArticle(
|
||||||
|
resourceType: KubernetesResourceType,
|
||||||
|
resourceAction: KubernetesResourceAction
|
||||||
|
) {
|
||||||
|
if (resourceAction === 'Delete' || resourceAction === 'Update') {
|
||||||
|
return 'the';
|
||||||
|
}
|
||||||
|
if (resourceAction === 'Create' && resourceType === 'Ingress') {
|
||||||
|
return 'an';
|
||||||
|
}
|
||||||
|
return 'a';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generateResourceSummaryList maps formValues to create and update summaries
|
||||||
|
*/
|
||||||
|
export function getAppResourceSummaries(
|
||||||
|
newFormValues: ApplicationFormValues,
|
||||||
|
oldFormValues?: ApplicationFormValues
|
||||||
|
): Array<Summary> {
|
||||||
|
if (!oldFormValues) {
|
||||||
|
return getCreatedApplicationResourcesNew(newFormValues);
|
||||||
|
}
|
||||||
|
return getUpdatedApplicationResources(newFormValues, oldFormValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCreatedApplicationResourcesNew(
|
||||||
|
formValues: ApplicationFormValues
|
||||||
|
): Array<Summary> {
|
||||||
|
// app summary
|
||||||
|
const appSummary: Summary = {
|
||||||
|
action: 'Create',
|
||||||
|
kind: formValues.ApplicationType,
|
||||||
|
name: formValues.Name,
|
||||||
|
};
|
||||||
|
|
||||||
|
// service summaries
|
||||||
|
const serviceFormSummaries: Array<Summary> =
|
||||||
|
formValues.Services?.map((service) => ({
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'Service',
|
||||||
|
name: service.Name || '',
|
||||||
|
type: service.Type,
|
||||||
|
})) || [];
|
||||||
|
// statefulsets require a headless service (https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#limitations)
|
||||||
|
// create a headless service summary if the application is a statefulset
|
||||||
|
const headlessSummary: Array<Summary> =
|
||||||
|
formValues.ApplicationType === 'StatefulSet'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'Service',
|
||||||
|
name: `headless-${formValues.Name}`,
|
||||||
|
type: 'ClusterIP',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
const serviceSummaries = [...serviceFormSummaries, ...headlessSummary];
|
||||||
|
|
||||||
|
// ingress summaries
|
||||||
|
const ingressesSummaries: Array<Summary> =
|
||||||
|
formValues.Services?.flatMap((service) => {
|
||||||
|
// a single service port can have multiple ingress paths (and even use different ingresses)
|
||||||
|
const servicePathsIngressNames = service.Ports.flatMap(
|
||||||
|
(port) => port.ingressPaths?.map((path) => path.IngressName) || []
|
||||||
|
);
|
||||||
|
const uniqueIngressNames = [...new Set(servicePathsIngressNames)];
|
||||||
|
return uniqueIngressNames.map((ingressName) => ({
|
||||||
|
action: 'Update',
|
||||||
|
kind: 'Ingress',
|
||||||
|
name: ingressName || '',
|
||||||
|
}));
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
// persistent volume claim (pvc) summaries
|
||||||
|
const pvcSummaries: Array<Summary> =
|
||||||
|
// apps with a isolated data access policy are statefulsets.
|
||||||
|
// statefulset pvcs are defined in spec.volumeClaimTemplates.
|
||||||
|
// https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#stable-storage
|
||||||
|
formValues.DataAccessPolicy === 'Shared'
|
||||||
|
? formValues.PersistedFolders?.map((volume) => ({
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'PersistentVolumeClaim',
|
||||||
|
name:
|
||||||
|
volume.existingVolume?.PersistentVolumeClaim.Name ||
|
||||||
|
volume.persistentVolumeClaimName ||
|
||||||
|
'',
|
||||||
|
})) || []
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// horizontal pod autoscaler summaries
|
||||||
|
const hpaSummary: Array<Summary> =
|
||||||
|
formValues.AutoScaler?.isUsed === true &&
|
||||||
|
formValues.DeploymentType !== 'Global'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'HorizontalPodAutoscaler',
|
||||||
|
name: formValues.Name,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
appSummary,
|
||||||
|
...serviceSummaries,
|
||||||
|
...ingressesSummaries,
|
||||||
|
...pvcSummaries,
|
||||||
|
...hpaSummary,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdatedApplicationResources(
|
||||||
|
newFormValues: ApplicationFormValues,
|
||||||
|
oldFormValues: ApplicationFormValues
|
||||||
|
) {
|
||||||
|
// app summaries
|
||||||
|
const updateAppSummaries: Array<Summary> =
|
||||||
|
oldFormValues.ApplicationType !== newFormValues.ApplicationType
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
action: 'Delete',
|
||||||
|
kind: oldFormValues.ApplicationType,
|
||||||
|
name: oldFormValues.Name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: newFormValues.ApplicationType,
|
||||||
|
name: newFormValues.Name,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
action: 'Update',
|
||||||
|
kind: newFormValues.ApplicationType,
|
||||||
|
name: newFormValues.Name,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// service summaries
|
||||||
|
const serviceSummaries: Array<Summary> = getServiceUpdateResourceSummary(
|
||||||
|
oldFormValues.Services,
|
||||||
|
newFormValues.Services
|
||||||
|
);
|
||||||
|
|
||||||
|
// ingress summaries
|
||||||
|
const oldServicePorts = oldFormValues.Services?.flatMap(
|
||||||
|
(service) => service.Ports
|
||||||
|
);
|
||||||
|
const oldIngresses = generateNewIngressesFromFormPaths(
|
||||||
|
oldFormValues.OriginalIngresses,
|
||||||
|
oldServicePorts,
|
||||||
|
oldServicePorts
|
||||||
|
);
|
||||||
|
const newServicePorts = newFormValues.Services?.flatMap(
|
||||||
|
(service) => service.Ports
|
||||||
|
);
|
||||||
|
const newIngresses = generateNewIngressesFromFormPaths(
|
||||||
|
newFormValues.OriginalIngresses,
|
||||||
|
newServicePorts,
|
||||||
|
oldServicePorts
|
||||||
|
);
|
||||||
|
const ingressSummaries = getIngressUpdateSummary(oldIngresses, newIngresses);
|
||||||
|
|
||||||
|
// persistent volume claim (pvc) summaries
|
||||||
|
const pvcSummaries: Array<Summary> =
|
||||||
|
// apps with a isolated data access policy are statefulsets.
|
||||||
|
// statefulset pvcs are defined in spec.volumeClaimTemplates.
|
||||||
|
// https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#stable-storage
|
||||||
|
newFormValues.DataAccessPolicy === 'Shared'
|
||||||
|
? newFormValues.PersistedFolders?.flatMap((newVolume) => {
|
||||||
|
const oldVolume = oldFormValues.PersistedFolders?.find(
|
||||||
|
(oldVolume) =>
|
||||||
|
oldVolume.persistentVolumeClaimName ===
|
||||||
|
newVolume.persistentVolumeClaimName
|
||||||
|
);
|
||||||
|
if (!oldVolume) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'PersistentVolumeClaim',
|
||||||
|
name:
|
||||||
|
newVolume.existingVolume?.PersistentVolumeClaim.Name ||
|
||||||
|
newVolume.persistentVolumeClaimName ||
|
||||||
|
'',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// updating a pvc is not supported
|
||||||
|
return [];
|
||||||
|
}) || []
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// TODO: horizontal pod autoscaler summaries
|
||||||
|
const createHPASummary: Array<Summary> =
|
||||||
|
newFormValues.AutoScaler?.isUsed && !oldFormValues.AutoScaler?.isUsed
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'HorizontalPodAutoscaler',
|
||||||
|
name: newFormValues.Name,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
const deleteHPASummary: Array<Summary> =
|
||||||
|
!newFormValues.AutoScaler?.isUsed && oldFormValues.AutoScaler?.isUsed
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
action: 'Delete',
|
||||||
|
kind: 'HorizontalPodAutoscaler',
|
||||||
|
name: oldFormValues.Name,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
const isHPAUpdated =
|
||||||
|
newFormValues.AutoScaler?.isUsed &&
|
||||||
|
oldFormValues.AutoScaler?.isUsed &&
|
||||||
|
(newFormValues.AutoScaler?.minReplicas !==
|
||||||
|
oldFormValues.AutoScaler?.minReplicas ||
|
||||||
|
newFormValues.AutoScaler?.maxReplicas !==
|
||||||
|
oldFormValues.AutoScaler?.maxReplicas ||
|
||||||
|
newFormValues.AutoScaler?.targetCpuUtilizationPercentage !==
|
||||||
|
oldFormValues.AutoScaler?.targetCpuUtilizationPercentage);
|
||||||
|
const updateHPASummary: Array<Summary> = isHPAUpdated
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
action: 'Update',
|
||||||
|
kind: 'HorizontalPodAutoscaler',
|
||||||
|
name: newFormValues.Name,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
const hpaSummaries = [
|
||||||
|
...createHPASummary,
|
||||||
|
...deleteHPASummary,
|
||||||
|
...updateHPASummary,
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...updateAppSummaries,
|
||||||
|
...serviceSummaries,
|
||||||
|
...ingressSummaries,
|
||||||
|
...pvcSummaries,
|
||||||
|
...hpaSummaries,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// getServiceUpdateResourceSummary replicates KubernetesServiceService.patch
|
||||||
|
function getServiceUpdateResourceSummary(
|
||||||
|
oldServices?: Array<ServiceFormValues>,
|
||||||
|
newServices?: Array<ServiceFormValues>
|
||||||
|
): Array<Summary> {
|
||||||
|
const updateAndCreateSummaries =
|
||||||
|
newServices?.flatMap<Summary>((newService) => {
|
||||||
|
const oldServiceMatched = oldServices?.find(
|
||||||
|
(oldService) => oldService.Name === newService.Name
|
||||||
|
);
|
||||||
|
if (oldServiceMatched) {
|
||||||
|
return getServiceUpdateSummary(oldServiceMatched, newService);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
action: 'Create',
|
||||||
|
kind: 'Service',
|
||||||
|
name: newService.Name || '',
|
||||||
|
type: newService.Type || 'ClusterIP',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
const deleteSummaries =
|
||||||
|
oldServices?.flatMap<Summary>((oldService) => {
|
||||||
|
const newServiceMatched = newServices?.find(
|
||||||
|
(newService) => newService.Name === oldService.Name
|
||||||
|
);
|
||||||
|
if (newServiceMatched) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
action: 'Delete',
|
||||||
|
kind: 'Service',
|
||||||
|
name: oldService.Name || '',
|
||||||
|
type: oldService.Type || 'ClusterIP',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return [...updateAndCreateSummaries, ...deleteSummaries];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServiceUpdateSummary(
|
||||||
|
oldService: ServiceFormValues,
|
||||||
|
newService: ServiceFormValues
|
||||||
|
): Array<Summary> {
|
||||||
|
const payload = getServicePatchPayload(oldService, newService);
|
||||||
|
if (payload.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
action: 'Update',
|
||||||
|
kind: 'Service',
|
||||||
|
name: oldService.Name || '',
|
||||||
|
type: oldService.Type || 'ClusterIP',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIngressUpdateSummary(
|
||||||
|
oldIngresses: Array<Ingress>,
|
||||||
|
newIngresses: Array<Ingress>
|
||||||
|
): Array<Summary> {
|
||||||
|
const ingressesSummaries = newIngresses.flatMap((newIng) => {
|
||||||
|
const oldIng = oldIngresses.find((oldIng) => oldIng.Name === newIng.Name);
|
||||||
|
if (oldIng) {
|
||||||
|
return getIngressUpdateResourceSummary(oldIng, newIng);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
return ingressesSummaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getIngressUpdateResourceSummary checks if any ingress paths have been changed
|
||||||
|
function getIngressUpdateResourceSummary(
|
||||||
|
oldIngress: Ingress,
|
||||||
|
newIngress: Ingress
|
||||||
|
): Array<Summary> {
|
||||||
|
const newIngressPaths = newIngress.Paths?.flatMap((path) => path.Path) || [];
|
||||||
|
const oldIngressPaths = oldIngress.Paths?.flatMap((path) => path.Path) || [];
|
||||||
|
const isAnyNewPathMissingOldPath = newIngressPaths.some(
|
||||||
|
(path) => !oldIngressPaths.includes(path)
|
||||||
|
);
|
||||||
|
const isAnyOldPathMissingNewPath = oldIngressPaths.some(
|
||||||
|
(path) => !newIngressPaths.includes(path)
|
||||||
|
);
|
||||||
|
if (isAnyNewPathMissingOldPath || isAnyOldPathMissingNewPath) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
action: 'Update',
|
||||||
|
kind: 'Ingress',
|
||||||
|
name: oldIngress.Name,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { StorageClass } from '@/react/portainer/environments/types';
|
import { StorageClass } from '@/react/portainer/environments/types';
|
||||||
|
@ -221,8 +220,7 @@ export function PersistedFolderItem({
|
||||||
function isToggleVolumeTypeVisible() {
|
function isToggleVolumeTypeVisible() {
|
||||||
return (
|
return (
|
||||||
!(isEdit && isExistingPersistedFolder()) && // if it's not an edit of an existing persisted folder
|
!(isEdit && isExistingPersistedFolder()) && // if it's not an edit of an existing persisted folder
|
||||||
applicationValues.ApplicationType !==
|
applicationValues.ApplicationType !== 'StatefulSet' && // and if it's not a statefulset
|
||||||
KubernetesApplicationTypes.STATEFULSET && // and if it's not a statefulset
|
|
||||||
applicationValues.Containers.length <= 1 // and if there is only one container);
|
applicationValues.Containers.length <= 1 // and if there is only one container);
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import uuidv4 from 'uuid/v4';
|
||||||
|
|
||||||
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
||||||
import { StorageClass } from '@/react/portainer/environments/types';
|
import { StorageClass } from '@/react/portainer/environments/types';
|
||||||
import { KubernetesApplicationTypes } from '@/kubernetes/models/application/models';
|
|
||||||
|
|
||||||
import { Option } from '@@/form-components/PortainerSelect';
|
import { Option } from '@@/form-components/PortainerSelect';
|
||||||
import { InlineLoader } from '@@/InlineLoader';
|
import { InlineLoader } from '@@/InlineLoader';
|
||||||
|
@ -43,11 +43,7 @@ export function PersistedFoldersFormSection({
|
||||||
const PVCOptions = usePVCOptions(availableVolumes);
|
const PVCOptions = usePVCOptions(availableVolumes);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSection
|
<FormSection title="Persisted folders" titleSize="sm">
|
||||||
title="Persisted folders"
|
|
||||||
titleSize="sm"
|
|
||||||
titleClassName="control-label !text-[0.9em]"
|
|
||||||
>
|
|
||||||
{storageClasses.length === 0 && (
|
{storageClasses.length === 0 && (
|
||||||
<TextTip color="blue">
|
<TextTip color="blue">
|
||||||
No storage option is available to persist data, contact your
|
No storage option is available to persist data, contact your
|
||||||
|
@ -81,17 +77,21 @@ export function PersistedFoldersFormSection({
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
itemBuilder={() => ({
|
itemBuilder={() => {
|
||||||
persistentVolumeClaimName:
|
const newVolumeClaimName = `${applicationValues.Name}-${uuidv4()}`;
|
||||||
availableVolumes[0]?.PersistentVolumeClaim.Name || '',
|
return {
|
||||||
containerPath: '',
|
persistentVolumeClaimName:
|
||||||
size: '',
|
availableVolumes[0]?.PersistentVolumeClaim.Name ||
|
||||||
sizeUnit: 'GB',
|
newVolumeClaimName,
|
||||||
storageClass: storageClasses[0],
|
containerPath: '',
|
||||||
useNewVolume: true,
|
size: '',
|
||||||
existingVolume: undefined,
|
sizeUnit: 'GB',
|
||||||
needsDeletion: false,
|
storageClass: storageClasses[0],
|
||||||
})}
|
useNewVolume: true,
|
||||||
|
existingVolume: undefined,
|
||||||
|
needsDeletion: false,
|
||||||
|
};
|
||||||
|
}}
|
||||||
addLabel="Add persisted folder"
|
addLabel="Add persisted folder"
|
||||||
canUndoDelete={isEdit}
|
canUndoDelete={isEdit}
|
||||||
/>
|
/>
|
||||||
|
@ -100,9 +100,7 @@ export function PersistedFoldersFormSection({
|
||||||
|
|
||||||
function isDeleteButtonHidden() {
|
function isDeleteButtonHidden() {
|
||||||
return (
|
return (
|
||||||
(isEdit &&
|
(isEdit && applicationValues.ApplicationType === 'StatefulSet') ||
|
||||||
applicationValues.ApplicationType ===
|
|
||||||
KubernetesApplicationTypes.STATEFULSET) ||
|
|
||||||
applicationValues.Containers.length >= 1
|
applicationValues.Containers.length >= 1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,11 +35,7 @@ export function PlacementFormSection({ values, onChange, errors }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<FormSection
|
<FormSection title="Placement preferences and constraints" titleSize="sm">
|
||||||
title="Placement preferences and constraints"
|
|
||||||
titleSize="sm"
|
|
||||||
titleClassName="control-label !text-[0.9em]"
|
|
||||||
>
|
|
||||||
{values.placements?.length > 0 && (
|
{values.placements?.length > 0 && (
|
||||||
<TextTip color="blue">
|
<TextTip color="blue">
|
||||||
Deploy this application on nodes that respect <b>ALL</b> of the
|
Deploy this application on nodes that respect <b>ALL</b> of the
|
||||||
|
|
|
@ -7,6 +7,7 @@ export const appOwnerLabel = 'io.portainer.kubernetes.application.owner';
|
||||||
export const appNoteAnnotation = 'io.portainer.kubernetes.application.note';
|
export const appNoteAnnotation = 'io.portainer.kubernetes.application.note';
|
||||||
export const appDeployMethodLabel = 'io.portainer.kubernetes.application.kind';
|
export const appDeployMethodLabel = 'io.portainer.kubernetes.application.kind';
|
||||||
export const defaultDeploymentUniqueLabel = 'pod-template-hash';
|
export const defaultDeploymentUniqueLabel = 'pod-template-hash';
|
||||||
|
export const appNameLabel = 'io.portainer.kubernetes.application.name';
|
||||||
|
|
||||||
export const appRevisionAnnotation = 'deployment.kubernetes.io/revision';
|
export const appRevisionAnnotation = 'deployment.kubernetes.io/revision';
|
||||||
|
|
||||||
|
@ -30,4 +31,4 @@ export const appKindToDeploymentTypeMap: Record<
|
||||||
StatefulSet: 'Replicated',
|
StatefulSet: 'Replicated',
|
||||||
DaemonSet: 'Global',
|
DaemonSet: 'Global',
|
||||||
Pod: null,
|
Pod: null,
|
||||||
};
|
} as const;
|
||||||
|
|
|
@ -11,9 +11,44 @@ import {
|
||||||
import { Pod, PodList } from 'kubernetes-types/core/v1';
|
import { Pod, PodList } from 'kubernetes-types/core/v1';
|
||||||
import { RawExtension } from 'kubernetes-types/runtime';
|
import { RawExtension } from 'kubernetes-types/runtime';
|
||||||
|
|
||||||
|
import { EnvVarValues } from '@@/form-components/EnvironmentVariablesFieldset';
|
||||||
|
|
||||||
|
import { Annotation } from '../annotations/types';
|
||||||
|
import { Ingress } from '../ingresses/types';
|
||||||
|
|
||||||
|
import { AutoScalingFormValues } from './components/AutoScalingFormSection/types';
|
||||||
|
import { ServiceFormValues } from './CreateView/application-services/types';
|
||||||
|
import { PersistedFolderFormValue } from './components/PersistedFoldersFormSection/types';
|
||||||
|
import { ConfigurationFormValues } from './components/ConfigurationsFormSection/types';
|
||||||
|
import {
|
||||||
|
Placement,
|
||||||
|
PlacementType,
|
||||||
|
} from './components/PlacementFormSection/types';
|
||||||
|
|
||||||
export type ApplicationFormValues = {
|
export type ApplicationFormValues = {
|
||||||
Containers: Array<unknown>;
|
Containers: Array<unknown>;
|
||||||
ApplicationType: number; // KubernetesApplicationTypes
|
ApplicationType: AppKind;
|
||||||
|
ResourcePool: unknown;
|
||||||
|
Name: string;
|
||||||
|
StackName?: string;
|
||||||
|
ApplicationOwner?: string;
|
||||||
|
ImageModel: unknown;
|
||||||
|
Note?: string;
|
||||||
|
MemoryLimit?: number;
|
||||||
|
CpuLimit?: number;
|
||||||
|
DeploymentType?: DeploymentType;
|
||||||
|
ReplicaCount?: number;
|
||||||
|
AutoScaler?: AutoScalingFormValues;
|
||||||
|
Services?: Array<ServiceFormValues>;
|
||||||
|
OriginalIngresses?: Array<Ingress>;
|
||||||
|
EnvironmentVariables?: EnvVarValues;
|
||||||
|
DataAccessPolicy?: AppDataAccessPolicy;
|
||||||
|
PersistedFolders?: Array<PersistedFolderFormValue>;
|
||||||
|
ConfigMaps?: Array<ConfigurationFormValues>;
|
||||||
|
Secrets?: Array<ConfigurationFormValues>;
|
||||||
|
PlacementType?: PlacementType;
|
||||||
|
Placements?: Array<Placement>;
|
||||||
|
Annotations?: Array<Annotation>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Application = Deployment | DaemonSet | StatefulSet | Pod;
|
export type Application = Deployment | DaemonSet | StatefulSet | Pod;
|
||||||
|
@ -30,8 +65,12 @@ export type ApplicationList =
|
||||||
|
|
||||||
export type AppKind = 'Deployment' | 'DaemonSet' | 'StatefulSet' | 'Pod';
|
export type AppKind = 'Deployment' | 'DaemonSet' | 'StatefulSet' | 'Pod';
|
||||||
|
|
||||||
|
export type AppType = AppKind | 'Helm';
|
||||||
|
|
||||||
export type DeploymentType = 'Replicated' | 'Global';
|
export type DeploymentType = 'Replicated' | 'Global';
|
||||||
|
|
||||||
|
export type AppDataAccessPolicy = 'Isolated' | 'Shared';
|
||||||
|
|
||||||
type Patch = {
|
type Patch = {
|
||||||
op: 'replace' | 'add' | 'remove';
|
op: 'replace' | 'add' | 'remove';
|
||||||
path: string;
|
path: string;
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { useSecrets } from '@/react/kubernetes/configs/secret.service';
|
||||||
import { useNamespaceServices } from '@/react/kubernetes/networks/services/queries';
|
import { useNamespaceServices } from '@/react/kubernetes/networks/services/queries';
|
||||||
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { useAuthorizations } from '@/react/hooks/useUser';
|
import { useAuthorizations } from '@/react/hooks/useUser';
|
||||||
|
import { Annotation } from '@/react/kubernetes/annotations/types';
|
||||||
|
import { prepareAnnotations } from '@/react/kubernetes/utils';
|
||||||
|
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
import { PageHeader } from '@@/PageHeader';
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
@ -22,7 +24,6 @@ import {
|
||||||
useUpdateIngress,
|
useUpdateIngress,
|
||||||
useIngressControllers,
|
useIngressControllers,
|
||||||
} from '../queries';
|
} from '../queries';
|
||||||
import { Annotation } from '../../annotations/types';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Rule,
|
Rule,
|
||||||
|
@ -35,7 +36,6 @@ import { IngressForm } from './IngressForm';
|
||||||
import {
|
import {
|
||||||
prepareTLS,
|
prepareTLS,
|
||||||
preparePaths,
|
preparePaths,
|
||||||
prepareAnnotations,
|
|
||||||
prepareRuleFromIngress,
|
prepareRuleFromIngress,
|
||||||
checkIfPathExistsWithHost,
|
checkIfPathExistsWithHost,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
|
@ -37,14 +37,6 @@ export function preparePaths(ingressName: string, hosts: Host[]) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareAnnotations(annotations: Annotation[]) {
|
|
||||||
const result: Record<string, string> = {};
|
|
||||||
annotations.forEach((a) => {
|
|
||||||
result[a.Key] = a.Value;
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSecretByHost(host: string, tls?: TLS[]) {
|
function getSecretByHost(host: string, tls?: TLS[]) {
|
||||||
let secret = '';
|
let secret = '';
|
||||||
if (tls) {
|
if (tls) {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Annotation } from './annotations/types';
|
||||||
|
|
||||||
export function parseCpu(cpu: string) {
|
export function parseCpu(cpu: string) {
|
||||||
let res = parseInt(cpu, 10);
|
let res = parseInt(cpu, 10);
|
||||||
if (cpu.endsWith('m')) {
|
if (cpu.endsWith('m')) {
|
||||||
|
@ -7,3 +9,14 @@ export function parseCpu(cpu: string) {
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function prepareAnnotations(annotations: Annotation[]) {
|
||||||
|
const result = annotations.reduce(
|
||||||
|
(acc, a) => {
|
||||||
|
acc[a.Key] = a.Value;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue