mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
refactor(app): migrate app summary section [EE-6239] (#10910)
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run
This commit is contained in:
parent
7a4314032a
commit
abf517de28
61 changed files with 1461 additions and 661 deletions
|
@ -2,11 +2,13 @@ import { BoxSelector } from '@@/BoxSelector';
|
|||
import { FormSection } from '@@/form-components/FormSection';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
import { DeploymentType } from '../types';
|
||||
|
||||
import { getDeploymentOptions } from './deploymentOptions';
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
onChange(value: number): void;
|
||||
value: DeploymentType;
|
||||
onChange(value: DeploymentType): void;
|
||||
supportGlobalDeployment: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { Box, Boxes } from 'lucide-react';
|
||||
|
||||
import { KubernetesApplicationDataAccessPolicies } from '@/kubernetes/models/application/models';
|
||||
|
||||
import { BoxSelector, BoxSelectorOption } from '@@/BoxSelector';
|
||||
|
||||
import { AppDataAccessPolicy } from '../types';
|
||||
|
||||
interface Props {
|
||||
isEdit: boolean;
|
||||
persistedFoldersUseExistingVolumes: boolean;
|
||||
value: number;
|
||||
onChange(value: number): void;
|
||||
value: AppDataAccessPolicy;
|
||||
onChange(value: AppDataAccessPolicy): void;
|
||||
}
|
||||
|
||||
export function DataAccessPolicyFormSection({
|
||||
|
@ -31,13 +31,13 @@ export function DataAccessPolicyFormSection({
|
|||
}
|
||||
|
||||
function getOptions(
|
||||
value: number,
|
||||
value: AppDataAccessPolicy,
|
||||
isEdit: boolean,
|
||||
persistedFoldersUseExistingVolumes: boolean
|
||||
): ReadonlyArray<BoxSelectorOption<number>> {
|
||||
): ReadonlyArray<BoxSelectorOption<AppDataAccessPolicy>> {
|
||||
return [
|
||||
{
|
||||
value: KubernetesApplicationDataAccessPolicies.ISOLATED,
|
||||
value: 'Isolated',
|
||||
id: 'data_access_isolated',
|
||||
icon: Boxes,
|
||||
iconType: 'badge',
|
||||
|
@ -49,12 +49,10 @@ function getOptions(
|
|||
? 'Changing the data access policy is not allowed'
|
||||
: '',
|
||||
disabled: () =>
|
||||
(isEdit &&
|
||||
value !== KubernetesApplicationDataAccessPolicies.ISOLATED) ||
|
||||
persistedFoldersUseExistingVolumes,
|
||||
(isEdit && value !== 'Isolated') || persistedFoldersUseExistingVolumes,
|
||||
},
|
||||
{
|
||||
value: KubernetesApplicationDataAccessPolicies.SHARED,
|
||||
value: 'Shared',
|
||||
id: 'data_access_shared',
|
||||
icon: Box,
|
||||
iconType: 'badge',
|
||||
|
@ -63,8 +61,7 @@ function getOptions(
|
|||
'Application will be deployed as a Deployment with a shared storage access',
|
||||
tooltip: () =>
|
||||
isEdit ? 'Changing the data access policy is not allowed' : '',
|
||||
disabled: () =>
|
||||
isEdit && value !== KubernetesApplicationDataAccessPolicies.SHARED,
|
||||
disabled: () => isEdit && value !== 'Shared',
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { FormikErrors } from 'formik';
|
||||
|
||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import {
|
||||
useIngressControllers,
|
||||
|
@ -10,12 +9,7 @@ import {
|
|||
|
||||
import { FormSection } from '@@/form-components/FormSection';
|
||||
|
||||
import {
|
||||
ServiceFormValues,
|
||||
ServiceTypeAngularEnum,
|
||||
ServiceTypeOption,
|
||||
ServiceTypeValue,
|
||||
} from './types';
|
||||
import { ServiceFormValues, ServiceTypeOption, ServiceType } from './types';
|
||||
import { generateUniqueName } from './utils';
|
||||
import { ClusterIpServicesForm } from './cluster-ip/ClusterIpServicesForm';
|
||||
import { ServiceTabs } from './components/ServiceTabs';
|
||||
|
@ -24,15 +18,6 @@ import { LoadBalancerServicesForm } from './load-balancer/LoadBalancerServicesFo
|
|||
import { ServiceTabLabel } from './components/ServiceTabLabel';
|
||||
import { PublishingExplaination } from './PublishingExplaination';
|
||||
|
||||
const serviceTypeEnumsToValues: Record<
|
||||
ServiceTypeAngularEnum,
|
||||
ServiceTypeValue
|
||||
> = {
|
||||
[KubernetesApplicationPublishingTypes.CLUSTER_IP]: 'ClusterIP',
|
||||
[KubernetesApplicationPublishingTypes.NODE_PORT]: 'NodePort',
|
||||
[KubernetesApplicationPublishingTypes.LOAD_BALANCER]: 'LoadBalancer',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
values: ServiceFormValues[];
|
||||
onChange: (services: ServiceFormValues[]) => void;
|
||||
|
@ -53,7 +38,7 @@ export function KubeServicesForm({
|
|||
namespace,
|
||||
}: Props) {
|
||||
const [selectedServiceType, setSelectedServiceType] =
|
||||
useState<ServiceTypeValue>('ClusterIP');
|
||||
useState<ServiceType>('ClusterIP');
|
||||
|
||||
// start loading ingresses and controllers early to reduce perceived loading time
|
||||
const environmentId = useEnvironmentId();
|
||||
|
@ -195,17 +180,17 @@ function getUniqNames(appName: string, services: ServiceFormValues[]) {
|
|||
*/
|
||||
function getServiceTypeCounts(
|
||||
services: ServiceFormValues[]
|
||||
): Record<ServiceTypeValue, number> {
|
||||
): Record<ServiceType, number> {
|
||||
return services.reduce(
|
||||
(acc, service) => {
|
||||
const type = serviceTypeEnumsToValues[service.Type];
|
||||
const type = service.Type;
|
||||
const count = acc[type];
|
||||
return {
|
||||
...acc,
|
||||
[type]: count ? count + 1 : 1,
|
||||
};
|
||||
},
|
||||
{} as Record<ServiceTypeValue, number>
|
||||
{} as Record<ServiceType, number>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -215,10 +200,10 @@ function getServiceTypeCounts(
|
|||
function getServiceTypeHasErrors(
|
||||
services: ServiceFormValues[],
|
||||
errors: FormikErrors<ServiceFormValues[] | undefined>
|
||||
): Record<ServiceTypeValue, boolean> {
|
||||
): Record<ServiceType, boolean> {
|
||||
return services.reduce(
|
||||
(acc, service, index) => {
|
||||
const type = serviceTypeEnumsToValues[service.Type];
|
||||
const type = service.Type;
|
||||
const serviceHasErrors = !!errors?.[index];
|
||||
// if the service type already has an error, don't overwrite it
|
||||
if (acc[type] === true) return acc;
|
||||
|
@ -228,6 +213,6 @@ function getServiceTypeHasErrors(
|
|||
[type]: serviceHasErrors,
|
||||
};
|
||||
},
|
||||
{} as Record<ServiceTypeValue, boolean>
|
||||
{} as Record<ServiceType, boolean>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -88,10 +88,7 @@ export function ClusterIpServiceForm({
|
|||
value={servicePort.targetPort}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
const newValue =
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value);
|
||||
const newValue = e.target.valueAsNumber;
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
targetPort: newValue,
|
||||
|
@ -113,10 +110,7 @@ export function ClusterIpServiceForm({
|
|||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
port: e.target.valueAsNumber,
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { Plus } from 'lucide-react';
|
||||
import { FormikErrors } from 'formik';
|
||||
|
||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
||||
|
||||
import { Card } from '@@/Card';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Button } from '@@/buttons';
|
||||
|
@ -36,8 +34,7 @@ export function ClusterIpServicesForm({
|
|||
isEditMode,
|
||||
}: Props) {
|
||||
const clusterIPServiceCount = services.filter(
|
||||
(service) =>
|
||||
service.Type === KubernetesApplicationPublishingTypes.CLUSTER_IP
|
||||
(service) => service.Type === 'ClusterIP'
|
||||
).length;
|
||||
return (
|
||||
<Card className="pb-5">
|
||||
|
@ -50,8 +47,7 @@ export function ClusterIpServicesForm({
|
|||
{clusterIPServiceCount > 0 && (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{services.map((service, index) =>
|
||||
service.Type ===
|
||||
KubernetesApplicationPublishingTypes.CLUSTER_IP ? (
|
||||
service.Type === 'ClusterIP' ? (
|
||||
<ClusterIpServiceForm
|
||||
key={index}
|
||||
serviceName={service.Name}
|
||||
|
@ -86,7 +82,7 @@ export function ClusterIpServicesForm({
|
|||
services.length + 1,
|
||||
services
|
||||
);
|
||||
newService.Type = KubernetesApplicationPublishingTypes.CLUSTER_IP;
|
||||
newService.Type = 'ClusterIP';
|
||||
const newServicePort = newPort(newService.Name);
|
||||
newService.Ports = [newServicePort];
|
||||
newService.Selector = selector;
|
||||
|
|
|
@ -22,7 +22,7 @@ export function ContainerPortInput({
|
|||
type="number"
|
||||
className="form-control min-w-max"
|
||||
name={`container_port_${portIndex}`}
|
||||
placeholder="80"
|
||||
placeholder="e.g. 80"
|
||||
min="1"
|
||||
max="65535"
|
||||
value={value ?? ''}
|
||||
|
|
|
@ -22,7 +22,7 @@ export function ServicePortInput({
|
|||
type="number"
|
||||
className="form-control min-w-max"
|
||||
name={`service_port_${portIndex}`}
|
||||
placeholder="80"
|
||||
placeholder="e.g. 80"
|
||||
min="1"
|
||||
max="65535"
|
||||
value={value ?? ''}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { ServiceTypeOption, ServiceTypeValue } from '../types';
|
||||
import { ServiceTypeOption, ServiceType } from '../types';
|
||||
|
||||
type Props = {
|
||||
serviceTypeOptions: ServiceTypeOption[];
|
||||
selectedServiceType: ServiceTypeValue;
|
||||
setSelectedServiceType: (serviceTypeValue: ServiceTypeValue) => void;
|
||||
selectedServiceType: ServiceType;
|
||||
setSelectedServiceType: (serviceTypeValue: ServiceType) => void;
|
||||
};
|
||||
|
||||
export function ServiceTabs({
|
||||
|
@ -32,7 +32,7 @@ export function ServiceTabs({
|
|||
value={serviceTypeOptions[index].value}
|
||||
checked={selectedServiceType === serviceTypeOptions[index].value}
|
||||
onChange={(e) =>
|
||||
setSelectedServiceType(e.target.value as ServiceTypeValue)
|
||||
setSelectedServiceType(e.target.value as ServiceType)
|
||||
}
|
||||
/>
|
||||
{label}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { SchemaOf, array, object, boolean, string, mixed, number } from 'yup';
|
||||
|
||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
||||
|
||||
import { ServiceFormValues, ServicePort } from './types';
|
||||
import { prependWithSlash } from './utils';
|
||||
|
||||
|
@ -46,11 +44,7 @@ export function kubeServicesValidation(
|
|||
Namespace: string(),
|
||||
Name: string(),
|
||||
StackName: string(),
|
||||
Type: mixed().oneOf([
|
||||
KubernetesApplicationPublishingTypes.CLUSTER_IP,
|
||||
KubernetesApplicationPublishingTypes.NODE_PORT,
|
||||
KubernetesApplicationPublishingTypes.LOAD_BALANCER,
|
||||
]),
|
||||
Type: mixed().oneOf(['ClusterIP', 'NodePort', 'LoadBalancer']),
|
||||
ClusterIP: string(),
|
||||
ApplicationName: string(),
|
||||
ApplicationOwner: string(),
|
||||
|
@ -61,6 +55,7 @@ export function kubeServicesValidation(
|
|||
object({
|
||||
port: number()
|
||||
.required('Service port number is required.')
|
||||
.typeError('Service port number is required.')
|
||||
.min(1, 'Service port number must be inside the range 1-65535.')
|
||||
.max(65535, 'Service port number must be inside the range 1-65535.')
|
||||
.test(
|
||||
|
@ -93,6 +88,7 @@ export function kubeServicesValidation(
|
|||
),
|
||||
targetPort: number()
|
||||
.required('Container port number is required.')
|
||||
.typeError('Container port number is required.')
|
||||
.min(1, 'Container port number must be inside the range 1-65535.')
|
||||
.max(
|
||||
65535,
|
||||
|
@ -116,8 +112,7 @@ export function kubeServicesValidation(
|
|||
);
|
||||
if (
|
||||
matchingService === undefined ||
|
||||
matchingService.Type !==
|
||||
KubernetesApplicationPublishingTypes.NODE_PORT // ignore validation unless the service is of type nodeport
|
||||
matchingService.Type !== 'NodePort'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
@ -143,8 +138,7 @@ export function kubeServicesValidation(
|
|||
|
||||
if (
|
||||
matchingService === undefined ||
|
||||
matchingService.Type !==
|
||||
KubernetesApplicationPublishingTypes.NODE_PORT // ignore validation unless the service is of type nodeport
|
||||
matchingService.Type !== 'NodePort'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
@ -163,8 +157,7 @@ export function kubeServicesValidation(
|
|||
const formNodePortsWithoutCurrentService = formServices
|
||||
.filter(
|
||||
(formService) =>
|
||||
formService.Type ===
|
||||
KubernetesApplicationPublishingTypes.NODE_PORT &&
|
||||
formService.Type === 'NodePort' &&
|
||||
formService.Name !== matchingService.Name
|
||||
)
|
||||
.flatMap((formService) => formService.Ports)
|
||||
|
@ -187,11 +180,7 @@ export function kubeServicesValidation(
|
|||
context.parent as ServicePort,
|
||||
formServices
|
||||
);
|
||||
if (
|
||||
!matchingService ||
|
||||
matchingService.Type !==
|
||||
KubernetesApplicationPublishingTypes.NODE_PORT
|
||||
) {
|
||||
if (!matchingService || matchingService.Type !== 'NodePort') {
|
||||
return true;
|
||||
}
|
||||
return nodePort >= 30000;
|
||||
|
@ -209,11 +198,7 @@ export function kubeServicesValidation(
|
|||
context.parent as ServicePort,
|
||||
formServices
|
||||
);
|
||||
if (
|
||||
!matchingService ||
|
||||
matchingService.Type !==
|
||||
KubernetesApplicationPublishingTypes.NODE_PORT
|
||||
) {
|
||||
if (!matchingService || matchingService.Type !== 'NodePort') {
|
||||
return true;
|
||||
}
|
||||
return nodePort <= 32767;
|
||||
|
|
|
@ -93,10 +93,7 @@ export function LoadBalancerServiceForm({
|
|||
value={servicePort.targetPort}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
const newValue =
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value);
|
||||
const newValue = e.target.valueAsNumber;
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
targetPort: newValue,
|
||||
|
@ -119,10 +116,7 @@ export function LoadBalancerServiceForm({
|
|||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
port: e.target.valueAsNumber,
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
|
@ -140,7 +134,7 @@ export function LoadBalancerServiceForm({
|
|||
type="number"
|
||||
className="form-control min-w-max"
|
||||
name={`loadbalancer_port_${portIndex}`}
|
||||
placeholder="80"
|
||||
placeholder="e.g. 80"
|
||||
min="1"
|
||||
max="65535"
|
||||
value={servicePort.port || ''}
|
||||
|
@ -148,10 +142,7 @@ export function LoadBalancerServiceForm({
|
|||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
port: e.target.valueAsNumber,
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Plus, RefreshCw } from 'lucide-react';
|
||||
import { FormikErrors } from 'formik';
|
||||
|
||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
|
@ -49,8 +48,7 @@ export function LoadBalancerServicesForm({
|
|||
);
|
||||
|
||||
const loadBalancerServiceCount = services.filter(
|
||||
(service) =>
|
||||
service.Type === KubernetesApplicationPublishingTypes.LOAD_BALANCER
|
||||
(service) => service.Type === 'LoadBalancer'
|
||||
).length;
|
||||
return (
|
||||
<Card className="pb-5">
|
||||
|
@ -95,8 +93,7 @@ export function LoadBalancerServicesForm({
|
|||
{loadBalancerServiceCount > 0 && (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{services.map((service, index) =>
|
||||
service.Type ===
|
||||
KubernetesApplicationPublishingTypes.LOAD_BALANCER ? (
|
||||
service.Type === 'LoadBalancer' ? (
|
||||
<LoadBalancerServiceForm
|
||||
key={index}
|
||||
serviceName={service.Name}
|
||||
|
@ -131,8 +128,7 @@ export function LoadBalancerServicesForm({
|
|||
services.length + 1,
|
||||
services
|
||||
);
|
||||
newService.Type =
|
||||
KubernetesApplicationPublishingTypes.LOAD_BALANCER;
|
||||
newService.Type = 'LoadBalancer';
|
||||
const newServicePort = newPort(newService.Name);
|
||||
newService.Ports = [newServicePort];
|
||||
newService.Selector = selector;
|
||||
|
|
|
@ -94,10 +94,7 @@ export function NodePortServiceForm({
|
|||
value={servicePort.targetPort}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
const newValue =
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value);
|
||||
const newValue = e.target.valueAsNumber;
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
targetPort: newValue,
|
||||
|
@ -120,10 +117,7 @@ export function NodePortServiceForm({
|
|||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
port: e.target.valueAsNumber,
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
|
@ -139,7 +133,7 @@ export function NodePortServiceForm({
|
|||
type="number"
|
||||
className="form-control min-w-max"
|
||||
name={`node_port_${portIndex}`}
|
||||
placeholder="30080"
|
||||
placeholder="e.g. 30080"
|
||||
min="30000"
|
||||
max="32767"
|
||||
value={servicePort.nodePort ?? ''}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { FormikErrors } from 'formik';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
||||
|
||||
import { Card } from '@@/Card';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Button } from '@@/buttons';
|
||||
|
@ -36,7 +34,7 @@ export function NodePortServicesForm({
|
|||
isEditMode,
|
||||
}: Props) {
|
||||
const nodePortServiceCount = services.filter(
|
||||
(service) => service.Type === KubernetesApplicationPublishingTypes.NODE_PORT
|
||||
(service) => service.Type === 'NodePort'
|
||||
).length;
|
||||
return (
|
||||
<Card className="pb-5">
|
||||
|
@ -48,8 +46,7 @@ export function NodePortServicesForm({
|
|||
{nodePortServiceCount > 0 && (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{services.map((service, index) =>
|
||||
service.Type ===
|
||||
KubernetesApplicationPublishingTypes.NODE_PORT ? (
|
||||
service.Type === 'NodePort' ? (
|
||||
<NodePortServiceForm
|
||||
key={index}
|
||||
serviceName={service.Name}
|
||||
|
@ -84,7 +81,7 @@ export function NodePortServicesForm({
|
|||
services.length + 1,
|
||||
services
|
||||
);
|
||||
newService.Type = KubernetesApplicationPublishingTypes.NODE_PORT;
|
||||
newService.Type = 'NodePort';
|
||||
const newServicePort = newPort(newService.Name);
|
||||
newService.Ports = [newServicePort];
|
||||
newService.Selector = selector;
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
||||
|
||||
export interface ServicePort {
|
||||
port?: number;
|
||||
port: number;
|
||||
targetPort?: number;
|
||||
nodePort?: number;
|
||||
serviceName?: string;
|
||||
|
@ -12,9 +10,6 @@ export interface ServicePort {
|
|||
ingressPaths?: ServicePortIngressPath[];
|
||||
}
|
||||
|
||||
export type ServiceTypeAngularEnum =
|
||||
(typeof KubernetesApplicationPublishingTypes)[keyof typeof KubernetesApplicationPublishingTypes];
|
||||
|
||||
export type ServicePortIngressPath = {
|
||||
IngressName?: string;
|
||||
Host?: string;
|
||||
|
@ -24,7 +19,7 @@ export type ServicePortIngressPath = {
|
|||
export type ServiceFormValues = {
|
||||
Headless: boolean;
|
||||
Ports: ServicePort[];
|
||||
Type: ServiceTypeAngularEnum;
|
||||
Type: ServiceType;
|
||||
Ingress: boolean;
|
||||
ClusterIP?: string;
|
||||
ApplicationName?: string;
|
||||
|
@ -36,9 +31,9 @@ export type ServiceFormValues = {
|
|||
Namespace?: string;
|
||||
};
|
||||
|
||||
export type ServiceTypeValue = 'ClusterIP' | 'NodePort' | 'LoadBalancer';
|
||||
export type ServiceType = 'ClusterIP' | 'NodePort' | 'LoadBalancer';
|
||||
export type ServiceTypeOption = {
|
||||
value: ServiceTypeValue;
|
||||
value: ServiceType;
|
||||
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';
|
||||
|
||||
export function newPort(serviceName?: string) {
|
||||
export function newPort(serviceName?: string): ServicePort {
|
||||
return {
|
||||
port: undefined,
|
||||
port: 80,
|
||||
targetPort: undefined,
|
||||
name: '',
|
||||
protocol: 'TCP',
|
||||
|
@ -43,7 +54,7 @@ export const serviceFormDefaultValues: ServiceFormValues = {
|
|||
Name: '',
|
||||
StackName: '',
|
||||
Ports: [],
|
||||
Type: 1, // clusterip type as default
|
||||
Type: 'ClusterIP',
|
||||
ClusterIP: '',
|
||||
ApplicationName: '',
|
||||
ApplicationOwner: '',
|
||||
|
@ -54,16 +65,16 @@ export const serviceFormDefaultValues: ServiceFormValues = {
|
|||
|
||||
/**
|
||||
* 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[]} oldServicesPorts - The old ServicePort objects
|
||||
* @returns {Ingress[]} The new Ingress objects
|
||||
* @returns {IngressFormValues[]} The new Ingress objects
|
||||
*/
|
||||
export function generateNewIngressesFromFormPaths(
|
||||
oldIngresses?: Ingress[],
|
||||
oldIngresses?: IngressFormValues[],
|
||||
newServicesPorts?: ServicePort[],
|
||||
oldServicesPorts?: ServicePort[]
|
||||
): Ingress[] {
|
||||
): IngressFormValues[] {
|
||||
// filter the ports to only the ones that have an ingress
|
||||
const oldIngressPaths = oldServicesPorts
|
||||
?.flatMap((port) => port.ingressPaths)
|
||||
|
@ -77,7 +88,7 @@ export function generateNewIngressesFromFormPaths(
|
|||
}
|
||||
|
||||
// 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) => {
|
||||
if (!oldIngressPath?.Path) return;
|
||||
const newMatchingIng = newIngresses?.find(
|
||||
|
@ -151,3 +162,57 @@ export function prependWithSlash(path?: string) {
|
|||
if (!path) return '';
|
||||
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 { KubernetesApplicationDeploymentTypes } from '@/kubernetes/models/application/models';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
||||
|
||||
import { DeploymentType } from '../types';
|
||||
|
||||
export function getDeploymentOptions(
|
||||
supportGlobalDeployment: boolean
|
||||
): ReadonlyArray<BoxSelectorOption<number>> {
|
||||
): ReadonlyArray<BoxSelectorOption<DeploymentType>> {
|
||||
return [
|
||||
{
|
||||
id: 'deployment_replicated',
|
||||
label: 'Replicated',
|
||||
value: KubernetesApplicationDeploymentTypes.REPLICATED,
|
||||
value: 'Replicated',
|
||||
icon: Sliders,
|
||||
iconType: 'badge',
|
||||
description: 'Run one or multiple instances of this container',
|
||||
|
@ -26,7 +26,7 @@ export function getDeploymentOptions(
|
|||
label: 'Global',
|
||||
description:
|
||||
'Application will be deployed as a DaemonSet with an instance on each node of the cluster',
|
||||
value: KubernetesApplicationDeploymentTypes.GLOBAL,
|
||||
value: 'Global',
|
||||
icon: Boxes,
|
||||
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 { StorageClass } from '@/react/portainer/environments/types';
|
||||
|
@ -221,8 +220,7 @@ export function PersistedFolderItem({
|
|||
function isToggleVolumeTypeVisible() {
|
||||
return (
|
||||
!(isEdit && isExistingPersistedFolder()) && // if it's not an edit of an existing persisted folder
|
||||
applicationValues.ApplicationType !==
|
||||
KubernetesApplicationTypes.STATEFULSET && // and if it's not a statefulset
|
||||
applicationValues.ApplicationType !== 'StatefulSet' && // and if it's not a statefulset
|
||||
applicationValues.Containers.length <= 1 // and if there is only one container);
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { FormikErrors } from 'formik';
|
||||
import { useMemo } from 'react';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
|
||||
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
||||
import { StorageClass } from '@/react/portainer/environments/types';
|
||||
import { KubernetesApplicationTypes } from '@/kubernetes/models/application/models';
|
||||
|
||||
import { Option } from '@@/form-components/PortainerSelect';
|
||||
import { InlineLoader } from '@@/InlineLoader';
|
||||
|
@ -43,11 +43,7 @@ export function PersistedFoldersFormSection({
|
|||
const PVCOptions = usePVCOptions(availableVolumes);
|
||||
|
||||
return (
|
||||
<FormSection
|
||||
title="Persisted folders"
|
||||
titleSize="sm"
|
||||
titleClassName="control-label !text-[0.9em]"
|
||||
>
|
||||
<FormSection title="Persisted folders" titleSize="sm">
|
||||
{storageClasses.length === 0 && (
|
||||
<TextTip color="blue">
|
||||
No storage option is available to persist data, contact your
|
||||
|
@ -81,17 +77,21 @@ export function PersistedFoldersFormSection({
|
|||
initialValues={initialValues}
|
||||
/>
|
||||
)}
|
||||
itemBuilder={() => ({
|
||||
persistentVolumeClaimName:
|
||||
availableVolumes[0]?.PersistentVolumeClaim.Name || '',
|
||||
containerPath: '',
|
||||
size: '',
|
||||
sizeUnit: 'GB',
|
||||
storageClass: storageClasses[0],
|
||||
useNewVolume: true,
|
||||
existingVolume: undefined,
|
||||
needsDeletion: false,
|
||||
})}
|
||||
itemBuilder={() => {
|
||||
const newVolumeClaimName = `${applicationValues.Name}-${uuidv4()}`;
|
||||
return {
|
||||
persistentVolumeClaimName:
|
||||
availableVolumes[0]?.PersistentVolumeClaim.Name ||
|
||||
newVolumeClaimName,
|
||||
containerPath: '',
|
||||
size: '',
|
||||
sizeUnit: 'GB',
|
||||
storageClass: storageClasses[0],
|
||||
useNewVolume: true,
|
||||
existingVolume: undefined,
|
||||
needsDeletion: false,
|
||||
};
|
||||
}}
|
||||
addLabel="Add persisted folder"
|
||||
canUndoDelete={isEdit}
|
||||
/>
|
||||
|
@ -100,9 +100,7 @@ export function PersistedFoldersFormSection({
|
|||
|
||||
function isDeleteButtonHidden() {
|
||||
return (
|
||||
(isEdit &&
|
||||
applicationValues.ApplicationType ===
|
||||
KubernetesApplicationTypes.STATEFULSET) ||
|
||||
(isEdit && applicationValues.ApplicationType === 'StatefulSet') ||
|
||||
applicationValues.Containers.length >= 1
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,11 +35,7 @@ export function PlacementFormSection({ values, onChange, errors }: Props) {
|
|||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<FormSection
|
||||
title="Placement preferences and constraints"
|
||||
titleSize="sm"
|
||||
titleClassName="control-label !text-[0.9em]"
|
||||
>
|
||||
<FormSection title="Placement preferences and constraints" titleSize="sm">
|
||||
{values.placements?.length > 0 && (
|
||||
<TextTip color="blue">
|
||||
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 appDeployMethodLabel = 'io.portainer.kubernetes.application.kind';
|
||||
export const defaultDeploymentUniqueLabel = 'pod-template-hash';
|
||||
export const appNameLabel = 'io.portainer.kubernetes.application.name';
|
||||
|
||||
export const appRevisionAnnotation = 'deployment.kubernetes.io/revision';
|
||||
|
||||
|
@ -30,4 +31,4 @@ export const appKindToDeploymentTypeMap: Record<
|
|||
StatefulSet: 'Replicated',
|
||||
DaemonSet: 'Global',
|
||||
Pod: null,
|
||||
};
|
||||
} as const;
|
||||
|
|
|
@ -11,9 +11,44 @@ import {
|
|||
import { Pod, PodList } from 'kubernetes-types/core/v1';
|
||||
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 = {
|
||||
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;
|
||||
|
@ -30,8 +65,12 @@ export type ApplicationList =
|
|||
|
||||
export type AppKind = 'Deployment' | 'DaemonSet' | 'StatefulSet' | 'Pod';
|
||||
|
||||
export type AppType = AppKind | 'Helm';
|
||||
|
||||
export type DeploymentType = 'Replicated' | 'Global';
|
||||
|
||||
export type AppDataAccessPolicy = 'Isolated' | 'Shared';
|
||||
|
||||
type Patch = {
|
||||
op: 'replace' | 'add' | 'remove';
|
||||
path: string;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue