2023-10-11 19:32:02 +00:00
import { useState , useEffect , useMemo , useCallback , ReactNode } from 'react' ;
2022-09-21 04:49:42 +00:00
import { useCurrentStateAndParams , useRouter } from '@uirouter/react' ;
import { v4 as uuidv4 } from 'uuid' ;
2023-03-01 00:11:12 +00:00
import { debounce } from 'lodash' ;
2022-09-21 04:49:42 +00:00
2022-11-13 08:10:18 +00:00
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId' ;
2022-09-21 04:49:42 +00:00
import { useConfigurations } from '@/react/kubernetes/configs/queries' ;
2023-08-27 21:01:35 +00:00
import { useNamespaceServices } from '@/react/kubernetes/networks/services/queries' ;
2023-08-01 07:31:35 +00:00
import { notifyError , notifySuccess } from '@/portainer/services/notifications' ;
2022-12-04 21:47:56 +00:00
import { useAuthorizations } from '@/react/hooks/useUser' ;
2022-09-21 04:49:42 +00:00
import { Link } from '@@/Link' ;
import { PageHeader } from '@@/PageHeader' ;
import { Option } from '@@/form-components/Input/Select' ;
import { Button } from '@@/buttons' ;
2023-10-11 19:32:02 +00:00
import { useNamespacesQuery } from '../../namespaces/queries/useNamespacesQuery' ;
2022-10-05 02:17:53 +00:00
import { Ingress , IngressController } from '../types' ;
2022-09-21 04:49:42 +00:00
import {
useCreateIngress ,
useIngresses ,
useUpdateIngress ,
useIngressControllers ,
} from '../queries' ;
2023-10-11 19:32:02 +00:00
import { Annotation } from '../../annotations/types' ;
2022-09-21 04:49:42 +00:00
2023-10-11 19:32:02 +00:00
import {
Rule ,
Path ,
Host ,
GroupedServiceOptions ,
IngressErrors ,
} from './types' ;
2022-09-21 04:49:42 +00:00
import { IngressForm } from './IngressForm' ;
import {
prepareTLS ,
preparePaths ,
prepareAnnotations ,
prepareRuleFromIngress ,
checkIfPathExistsWithHost ,
} from './utils' ;
export function CreateIngressView() {
const environmentId = useEnvironmentId ( ) ;
const { params } = useCurrentStateAndParams ( ) ;
2022-12-04 21:47:56 +00:00
const isAuthorisedToAddEdit = useAuthorizations ( [ 'K8sIngressesW' ] ) ;
2022-09-21 04:49:42 +00:00
const router = useRouter ( ) ;
const isEdit = ! ! params . namespace ;
2022-12-04 21:47:56 +00:00
useEffect ( ( ) = > {
if ( ! isAuthorisedToAddEdit ) {
const message = ` Not authorized to ${ isEdit ? 'edit' : 'add' } ingresses ` ;
notifyError ( 'Error' , new Error ( message ) ) ;
router . stateService . go ( 'kubernetes.ingresses' ) ;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
} , [ isAuthorisedToAddEdit , isEdit ] ) ;
2022-09-21 04:49:42 +00:00
const [ namespace , setNamespace ] = useState < string > ( params . namespace || '' ) ;
const [ ingressRule , setIngressRule ] = useState < Rule > ( { } as Rule ) ;
2023-08-16 06:07:46 +00:00
// isEditClassNameSet is used to prevent premature validation of the classname in the edit view
const [ isEditClassNameSet , setIsEditClassNameSet ] = useState < boolean > ( false ) ;
2022-09-21 04:49:42 +00:00
2023-10-11 19:32:02 +00:00
const [ errors , setErrors ] = useState < IngressErrors > ( { } ) ;
2022-09-21 04:49:42 +00:00
2023-10-11 19:32:02 +00:00
const { data : namespaces , . . . namespacesQuery } =
useNamespacesQuery ( environmentId ) ;
2022-09-21 04:49:42 +00:00
2023-08-27 21:01:35 +00:00
const { data : allServices } = useNamespaceServices ( environmentId , namespace ) ;
2022-09-21 04:49:42 +00:00
const configResults = useConfigurations ( environmentId , namespace ) ;
const ingressesResults = useIngresses (
environmentId ,
2023-08-01 07:31:35 +00:00
namespaces ? Object . keys ( namespaces || { } ) : [ ]
2022-09-21 04:49:42 +00:00
) ;
2023-08-03 11:03:09 +00:00
const { data : ingressControllers , . . . ingressControllersQuery } =
useIngressControllers ( environmentId , namespace ) ;
2022-09-21 04:49:42 +00:00
const createIngressMutation = useCreateIngress ( ) ;
const updateIngressMutation = useUpdateIngress ( ) ;
const [ ingressNames , ingresses , ruleCounterByNamespace , hostWithTLS ] =
useMemo ( ( ) : [
string [ ] ,
Ingress [ ] ,
Record < string , number > ,
2023-09-04 15:20:36 +00:00
Record < string , string > ,
2022-09-21 04:49:42 +00:00
] = > {
const ruleCounterByNamespace : Record < string , number > = { } ;
const hostWithTLS : Record < string , string > = { } ;
ingressesResults . data ? . forEach ( ( ingress ) = > {
ingress . TLS ? . forEach ( ( tls ) = > {
tls . Hosts . forEach ( ( host ) = > {
hostWithTLS [ host ] = tls . SecretName ;
} ) ;
} ) ;
} ) ;
const ingressNames : string [ ] = [ ] ;
ingressesResults . data ? . forEach ( ( ing ) = > {
ruleCounterByNamespace [ ing . Namespace ] =
ruleCounterByNamespace [ ing . Namespace ] || 0 ;
const n = ing . Name . match ( /^(.*)-(\d+)$/ ) ;
if ( n ? . length === 3 ) {
ruleCounterByNamespace [ ing . Namespace ] = Math . max (
ruleCounterByNamespace [ ing . Namespace ] ,
Number ( n [ 2 ] )
) ;
}
if ( ing . Namespace === namespace ) {
ingressNames . push ( ing . Name ) ;
}
} ) ;
return [
ingressNames || [ ] ,
ingressesResults . data || [ ] ,
ruleCounterByNamespace ,
hostWithTLS ,
] ;
} , [ ingressesResults . data , namespace ] ) ;
2023-08-01 07:31:35 +00:00
const namespaceOptions = useMemo (
2022-09-21 04:49:42 +00:00
( ) = >
2023-08-01 07:31:35 +00:00
Object . entries ( namespaces || { } )
. filter ( ( [ , nsValue ] ) = > ! nsValue . IsSystem )
. map ( ( [ nsKey ] ) = > ( {
label : nsKey ,
value : nsKey ,
} ) ) ,
[ namespaces ]
2022-09-21 04:49:42 +00:00
) ;
2023-08-01 07:31:35 +00:00
const serviceOptions : GroupedServiceOptions = useMemo ( ( ) = > {
const groupedOptions : GroupedServiceOptions = (
allServices ? . reduce < GroupedServiceOptions > (
( groupedOptions , service ) = > {
// add a new option to the group that matches the service type
const newGroupedOptions = groupedOptions . map ( ( group ) = > {
if ( group . label === service . Type ) {
return {
. . . group ,
options : [
. . . group . options ,
2023-08-14 00:34:58 +00:00
{
label : service.Name ,
selectedLabel : ` ${ service . Name } ( ${ service . Type } ) ` ,
value : service.Name ,
} ,
2023-08-01 07:31:35 +00:00
] ,
} ;
}
return group ;
} ) ;
return newGroupedOptions ;
} ,
[
{ label : 'ClusterIP' , options : [ ] } ,
{ label : 'NodePort' , options : [ ] } ,
{ label : 'LoadBalancer' , options : [ ] } ,
] as GroupedServiceOptions
) || [ ]
) . filter ( ( group ) = > group . options . length > 0 ) ;
return groupedOptions ;
} , [ allServices ] ) ;
2022-10-07 03:55:11 +00:00
const servicePorts = useMemo (
( ) = >
2023-08-01 07:31:35 +00:00
allServices
2022-10-07 03:55:11 +00:00
? Object . fromEntries (
2023-08-01 07:31:35 +00:00
allServices ? . map ( ( service ) = > [
2022-10-07 03:55:11 +00:00
service . Name ,
service . Ports . map ( ( port ) = > ( {
label : String ( port . Port ) ,
value : String ( port . Port ) ,
} ) ) ,
] )
)
: { } ,
2023-08-01 07:31:35 +00:00
[ allServices ]
2022-10-07 03:55:11 +00:00
) ;
2022-09-21 04:49:42 +00:00
const existingIngressClass = useMemo (
( ) = >
2023-08-03 11:03:09 +00:00
ingressControllers ? . find (
( controller ) = >
controller . ClassName === ingressRule . IngressClassName ||
( controller . Type === 'custom' && ingressRule . IngressClassName === '' )
2022-09-21 04:49:42 +00:00
) ,
2023-08-03 11:03:09 +00:00
[ ingressControllers , ingressRule . IngressClassName ]
2023-08-01 07:31:35 +00:00
) ;
2023-08-03 11:03:09 +00:00
const ingressClassOptions : Option < string > [ ] = useMemo ( ( ) = > {
const allowedIngressClassOptions =
ingressControllers
? . filter ( ( controller ) = > ! ! controller . Availability )
2023-08-01 07:31:35 +00:00
. map ( ( cls ) = > ( {
label : cls.ClassName ,
value : cls.ClassName ,
2023-08-03 11:03:09 +00:00
} ) ) || [ ] ;
// if the ingress class is not set, return only the allowed ingress classes
if ( ingressRule . IngressClassName === '' || ! isEdit ) {
return allowedIngressClassOptions ;
}
// if the ingress class is set and it exists (even if disallowed), return the allowed ingress classes + the disallowed option
const disallowedIngressClasses =
ingressControllers
? . filter (
( controller ) = >
! controller . Availability &&
existingIngressClass ? . ClassName === controller . ClassName
)
. map ( ( controller ) = > ( {
label : ` ${ controller . ClassName } - DISALLOWED ` ,
value : controller.ClassName ,
} ) ) || [ ] ;
const existingIngressClassFound = ingressControllers ? . find (
( controller ) = > existingIngressClass ? . ClassName === controller . ClassName
) ;
if ( existingIngressClassFound ) {
return [ . . . allowedIngressClassOptions , . . . disallowedIngressClasses ] ;
}
// if the ingress class is set and it doesn't exist, return the allowed ingress classes + the not found option
const notFoundIngressClassOption = {
label : ` ${ ingressRule . IngressClassName } - NOT FOUND ` ,
value : ingressRule.IngressClassName || '' ,
} ;
return [ . . . allowedIngressClassOptions , notFoundIngressClassOption ] ;
} , [
existingIngressClass ? . ClassName ,
ingressControllers ,
ingressRule . IngressClassName ,
isEdit ,
] ) ;
const handleIngressChange = useCallback (
( key : string , val : string ) = > {
setIngressRule ( ( prevRules ) = > {
const rule = { . . . prevRules , [ key ] : val } ;
if ( key === 'IngressClassName' ) {
rule . IngressType = ingressControllers ? . find (
( c ) = > c . ClassName === val
) ? . Type ;
}
return rule ;
} ) ;
} ,
[ ingressControllers ]
2022-09-21 04:49:42 +00:00
) ;
2023-08-16 06:07:46 +00:00
// when them selected ingress class option update is no longer available set to an empty value
2023-08-03 11:03:09 +00:00
useEffect ( ( ) = > {
const ingressClasses = ingressClassOptions . map ( ( option ) = > option . value ) ;
2023-08-16 06:07:46 +00:00
if (
! ingressClasses . includes ( ingressRule . IngressClassName ) &&
ingressControllersQuery . isSuccess
) {
handleIngressChange ( 'IngressClassName' , '' ) ;
2023-08-03 11:03:09 +00:00
}
2023-08-16 06:07:46 +00:00
} , [
handleIngressChange ,
ingressClassOptions ,
ingressControllersQuery . isSuccess ,
ingressRule . IngressClassName ,
] ) ;
2022-09-21 04:49:42 +00:00
const matchedConfigs = configResults ? . data ? . filter (
( config ) = >
config . SecretType === 'kubernetes.io/tls' &&
config . Namespace === namespace
) ;
2022-10-09 20:32:30 +00:00
const tlsOptions : Option < string > [ ] = useMemo (
( ) = > [
{ label : 'No TLS' , value : '' } ,
. . . ( matchedConfigs ? . map ( ( config ) = > ( {
label : config.Name ,
value : config.Name ,
} ) ) || [ ] ) ,
] ,
[ matchedConfigs ]
) ;
2022-09-21 04:49:42 +00:00
useEffect ( ( ) = > {
2022-10-05 02:17:53 +00:00
if (
! ! params . name &&
ingressesResults . data &&
! ingressRule . IngressName &&
2023-08-01 07:31:35 +00:00
! ingressControllersQuery . isLoading &&
! ingressControllersQuery . isLoading
2022-10-05 02:17:53 +00:00
) {
2022-09-21 04:49:42 +00:00
// if it is an edit screen, prepare the rule from the ingress
const ing = ingressesResults . data ? . find (
( ing ) = > ing . Name === params . name && ing . Namespace === params . namespace
) ;
if ( ing ) {
2023-08-03 11:03:09 +00:00
const type = ingressControllers ? . find (
2022-10-24 20:41:30 +00:00
( c ) = >
c . ClassName === ing . ClassName ||
( c . Type === 'custom' && ! ing . ClassName )
2022-09-21 04:49:42 +00:00
) ? . Type ;
2022-10-24 20:41:30 +00:00
const r = prepareRuleFromIngress ( ing , type ) ;
2022-10-05 02:17:53 +00:00
r . IngressType = type || r . IngressType ;
2022-09-21 04:49:42 +00:00
setIngressRule ( r ) ;
2023-08-16 06:07:46 +00:00
setIsEditClassNameSet ( true ) ;
2022-09-21 04:49:42 +00:00
}
}
2022-10-16 21:44:17 +00:00
// eslint-disable-next-line react-hooks/exhaustive-deps
2022-09-21 04:49:42 +00:00
} , [
params . name ,
ingressesResults . data ,
2023-08-03 11:03:09 +00:00
ingressControllers ,
2022-09-21 04:49:42 +00:00
ingressRule . IngressName ,
params . namespace ,
] ) ;
2022-10-09 20:32:30 +00:00
useEffect ( ( ) = > {
// for each host, if the tls selection doesn't exist as an option, change it to the first option
if ( ingressRule ? . Hosts ? . length ) {
ingressRule . Hosts . forEach ( ( host , hIndex ) = > {
const secret = host . Secret || '' ;
const tlsOptionVals = tlsOptions . map ( ( o ) = > o . value ) ;
if ( tlsOptions ? . length && ! tlsOptionVals ? . includes ( secret ) ) {
handleTLSChange ( hIndex , tlsOptionVals [ 0 ] ) ;
}
} ) ;
}
} , [ tlsOptions , ingressRule . Hosts ] ) ;
2022-10-07 03:55:11 +00:00
useEffect ( ( ) = > {
// for each path in each host, if the service port doesn't exist as an option, change it to the first option
if ( ingressRule ? . Hosts ? . length ) {
ingressRule . Hosts . forEach ( ( host , hIndex ) = > {
host ? . Paths ? . forEach ( ( path , pIndex ) = > {
const serviceName = path . ServiceName ;
const currentServicePorts = servicePorts [ serviceName ] ? . map (
( p ) = > p . value
) ;
if (
currentServicePorts ? . length &&
! currentServicePorts ? . includes ( String ( path . ServicePort ) )
) {
handlePathChange (
hIndex ,
pIndex ,
'ServicePort' ,
currentServicePorts [ 0 ]
) ;
}
} ) ;
} ) ;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
} , [ ingressRule , servicePorts ] ) ;
2023-03-01 00:11:12 +00:00
const validate = useCallback (
(
ingressRule : Rule ,
ingressNames : string [ ] ,
2023-08-01 07:31:35 +00:00
groupedServiceOptions : GroupedServiceOptions ,
2023-03-01 00:11:12 +00:00
existingIngressClass? : IngressController
) = > {
const errors : Record < string , ReactNode > = { } ;
const rule = { . . . ingressRule } ;
// User cannot edit the namespace and the ingress name
if ( ! isEdit ) {
if ( ! rule . Namespace ) {
errors . namespace = 'Namespace is required' ;
}
const nameRegex = /^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/ ;
if ( ! rule . IngressName ) {
errors . ingressName = 'Ingress name is required' ;
} else if ( ! nameRegex . test ( rule . IngressName ) ) {
errors . ingressName =
"This field must consist of lower case alphanumeric characters or '-', contain at most 63 characters, start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123')." ;
} else if ( ingressNames . includes ( rule . IngressName ) ) {
errors . ingressName = 'Ingress name already exists' ;
}
2023-08-16 06:07:46 +00:00
if (
( ! ingressClassOptions . length || ! rule . IngressClassName ) &&
ingressControllersQuery . isSuccess
) {
2023-03-01 00:11:12 +00:00
errors . className = 'Ingress class is required' ;
}
}
2023-08-16 06:07:46 +00:00
if ( isEdit && ! ingressRule . IngressClassName && isEditClassNameSet ) {
2023-03-01 00:11:12 +00:00
errors . className =
'No ingress class is currently set for this ingress - use of the Portainer UI requires one to be set.' ;
}
if (
isEdit &&
( ! existingIngressClass ||
( existingIngressClass && ! existingIngressClass . Availability ) ) &&
ingressRule . IngressClassName
) {
if ( ! rule . IngressType ) {
errors . className =
'Currently set to an ingress class that cannot be found in the cluster - you must select a valid class.' ;
} else {
errors . className =
'Currently set to an ingress class that you do not have access to - you must select a valid class.' ;
}
}
const duplicatedAnnotations : string [ ] = [ ] ;
2023-04-26 04:48:55 +00:00
const re = /^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$/ ;
2023-03-01 00:11:12 +00:00
rule . Annotations ? . forEach ( ( a , i ) = > {
if ( ! a . Key ) {
2023-04-26 04:48:55 +00:00
errors [ ` annotations.key[ ${ i } ] ` ] = 'Key is required.' ;
2023-03-01 00:11:12 +00:00
} else if ( duplicatedAnnotations . includes ( a . Key ) ) {
2023-04-26 04:48:55 +00:00
errors [ ` annotations.key[ ${ i } ] ` ] =
'Key is a duplicate of an existing one.' ;
} else {
const key = a . Key . split ( '/' ) ;
if ( key . length > 2 ) {
errors [ ` annotations.key[ ${ i } ] ` ] =
'Two segments are allowed, separated by a slash (/): a prefix (optional) and a name.' ;
} else if ( key . length === 2 ) {
if ( key [ 0 ] . length > 253 ) {
errors [ ` annotations.key[ ${ i } ] ` ] =
"Prefix (before the slash) can't exceed 253 characters." ;
} else if ( key [ 1 ] . length > 63 ) {
errors [ ` annotations.key[ ${ i } ] ` ] =
"Name (after the slash) can't exceed 63 characters." ;
} else if ( ! re . test ( key [ 1 ] ) ) {
errors [ ` annotations.key[ ${ i } ] ` ] =
'Start and end with alphanumeric characters only, limiting characters in between to dashes, underscores, and alphanumerics.' ;
}
} else if ( key . length === 1 ) {
if ( key [ 0 ] . length > 63 ) {
errors [ ` annotations.key[ ${ i } ] ` ] =
"Name (the segment after a slash (/), or only segment if no slash) can't exceed 63 characters." ;
} else if ( ! re . test ( key [ 0 ] ) ) {
errors [ ` annotations.key[ ${ i } ] ` ] =
'Start and end with alphanumeric characters only, limiting characters in between to dashes, underscores, and alphanumerics.' ;
}
}
2023-03-01 00:11:12 +00:00
}
if ( ! a . Value ) {
2023-04-26 04:48:55 +00:00
errors [ ` annotations.value[ ${ i } ] ` ] = 'Value is required.' ;
2023-03-01 00:11:12 +00:00
}
duplicatedAnnotations . push ( a . Key ) ;
} ) ;
const duplicatedHosts : string [ ] = [ ] ;
// Check if the paths are duplicates
rule . Hosts ? . forEach ( ( host , hi ) = > {
if ( ! host . NoHost ) {
if ( ! host . Host ) {
errors [ ` hosts[ ${ hi } ].host ` ] = 'Host is required' ;
} else if ( duplicatedHosts . includes ( host . Host ) ) {
errors [ ` hosts[ ${ hi } ].host ` ] = 'Host cannot be duplicated' ;
}
duplicatedHosts . push ( host . Host ) ;
}
// Validate service
host . Paths ? . forEach ( ( path , pi ) = > {
if ( ! path . ServiceName ) {
errors [ ` hosts[ ${ hi } ].paths[ ${ pi } ].servicename ` ] =
'Service name is required' ;
}
2023-08-01 07:31:35 +00:00
const availableServiceNames = groupedServiceOptions . flatMap (
( optionGroup ) = > optionGroup . options . map ( ( option ) = > option . value )
) ;
2023-03-01 00:11:12 +00:00
if (
isEdit &&
path . ServiceName &&
2023-08-01 07:31:35 +00:00
! availableServiceNames . find ( ( sn ) = > sn === path . ServiceName )
2023-03-01 00:11:12 +00:00
) {
errors [ ` hosts[ ${ hi } ].paths[ ${ pi } ].servicename ` ] = (
< span >
Currently set to { path . ServiceName } , which does not exist . You
can create a service with this name for a particular deployment
via { ' ' }
< Link
to = "kubernetes.applications"
params = { { id : environmentId } }
className = "text-primary"
target = "_blank"
>
Applications
< / Link >
, and on returning here it will be picked up .
< / span >
) ;
}
if ( ! path . ServicePort ) {
errors [ ` hosts[ ${ hi } ].paths[ ${ pi } ].serviceport ` ] =
'Service port is required' ;
}
} ) ;
// Validate paths
const paths = host . Paths . map ( ( path ) = > path . Route ) ;
paths . forEach ( ( item , idx ) = > {
if ( ! item ) {
errors [ ` hosts[ ${ hi } ].paths[ ${ idx } ].path ` ] = 'Path cannot be empty' ;
} else if ( paths . indexOf ( item ) !== idx ) {
errors [ ` hosts[ ${ hi } ].paths[ ${ idx } ].path ` ] =
'Paths cannot be duplicated' ;
} else {
// Validate host and path combination globally
const isExists = checkIfPathExistsWithHost (
ingresses ,
host . Host ,
item ,
params . name
) ;
if ( isExists ) {
errors [ ` hosts[ ${ hi } ].paths[ ${ idx } ].path ` ] =
'Path is already in use with the same host' ;
}
}
} ) ;
} ) ;
setErrors ( errors ) ;
if ( Object . keys ( errors ) . length > 0 ) {
return false ;
}
return true ;
} ,
2023-08-01 07:31:35 +00:00
[
isEdit ,
2023-08-16 06:07:46 +00:00
isEditClassNameSet ,
ingressClassOptions . length ,
2023-08-01 07:31:35 +00:00
ingressControllersQuery . isSuccess ,
environmentId ,
ingresses ,
params . name ,
]
2023-03-01 00:11:12 +00:00
) ;
2023-08-01 07:31:35 +00:00
const debouncedValidate = useMemo ( ( ) = > debounce ( validate , 500 ) , [ validate ] ) ;
2023-03-01 00:11:12 +00:00
2022-09-21 04:49:42 +00:00
useEffect ( ( ) = > {
if ( namespace . length > 0 ) {
2023-03-01 00:11:12 +00:00
debouncedValidate (
2022-09-21 04:49:42 +00:00
ingressRule ,
ingressNames || [ ] ,
2023-08-01 07:31:35 +00:00
serviceOptions || [ ] ,
2022-10-05 02:17:53 +00:00
existingIngressClass
2022-09-21 04:49:42 +00:00
) ;
}
} , [
ingressRule ,
namespace ,
ingressNames ,
2023-08-01 07:31:35 +00:00
serviceOptions ,
2022-09-21 04:49:42 +00:00
existingIngressClass ,
2023-03-01 00:11:12 +00:00
debouncedValidate ,
2022-09-21 04:49:42 +00:00
] ) ;
return (
< >
< PageHeader
2023-08-14 00:34:58 +00:00
title = { isEdit ? 'Edit ingress' : 'Create ingress' }
2022-09-21 04:49:42 +00:00
breadcrumbs = { [
{
link : 'kubernetes.ingresses' ,
label : 'Ingresses' ,
} ,
{
2023-08-14 00:34:58 +00:00
label : isEdit ? 'Edit ingress' : 'Create ingress' ,
2022-09-21 04:49:42 +00:00
} ,
] }
/ >
< div className = "row ingress-rules" >
< div className = "col-sm-12" >
< IngressForm
environmentID = { environmentId }
isEdit = { isEdit }
rule = { ingressRule }
ingressClassOptions = { ingressClassOptions }
2023-08-01 07:31:35 +00:00
isIngressClassOptionsLoading = { ingressControllersQuery . isLoading }
2022-09-21 04:49:42 +00:00
errors = { errors }
servicePorts = { servicePorts }
tlsOptions = { tlsOptions }
serviceOptions = { serviceOptions }
addNewIngressHost = { addNewIngressHost }
handleTLSChange = { handleTLSChange }
handleHostChange = { handleHostChange }
handleIngressChange = { handleIngressChange }
handlePathChange = { handlePathChange }
addNewIngressRoute = { addNewIngressRoute }
removeIngressHost = { removeIngressHost }
removeIngressRoute = { removeIngressRoute }
addNewAnnotation = { addNewAnnotation }
removeAnnotation = { removeAnnotation }
reloadTLSCerts = { reloadTLSCerts }
handleAnnotationChange = { handleAnnotationChange }
namespace = { namespace }
handleNamespaceChange = { handleNamespaceChange }
2023-08-01 07:31:35 +00:00
namespacesOptions = { namespaceOptions }
isNamespaceOptionsLoading = { namespacesQuery . isLoading }
2023-08-16 06:07:46 +00:00
// wait for ingress results too to set a name that's not taken with handleNamespaceChange()
isIngressNamesLoading = { ingressesResults . isLoading }
2022-09-21 04:49:42 +00:00
/ >
< / div >
2023-08-01 07:31:35 +00:00
{ namespace && (
2022-09-21 04:49:42 +00:00
< div className = "col-sm-12" >
< Button
onClick = { ( ) = > handleCreateIngressRules ( ) }
disabled = { Object . keys ( errors ) . length > 0 }
>
{ isEdit ? 'Update' : 'Create' }
< / Button >
< / div >
) }
< / div >
< / >
) ;
function handleNamespaceChange ( ns : string ) {
setNamespace ( ns ) ;
if ( ! isEdit ) {
addNewIngress ( ns ) ;
}
}
function handleTLSChange ( hostIndex : number , tls : string ) {
setIngressRule ( ( prevRules ) = > {
const rule = { . . . prevRules } ;
rule . Hosts [ hostIndex ] = { . . . rule . Hosts [ hostIndex ] , Secret : tls } ;
return rule ;
} ) ;
}
function handleHostChange ( hostIndex : number , val : string ) {
setIngressRule ( ( prevRules ) = > {
const rule = { . . . prevRules } ;
rule . Hosts [ hostIndex ] = { . . . rule . Hosts [ hostIndex ] , Host : val } ;
rule . Hosts [ hostIndex ] . Secret =
hostWithTLS [ val ] || rule . Hosts [ hostIndex ] . Secret ;
return rule ;
} ) ;
}
function handlePathChange (
hostIndex : number ,
pathIndex : number ,
key : 'Route' | 'PathType' | 'ServiceName' | 'ServicePort' ,
val : string
) {
setIngressRule ( ( prevRules ) = > {
const rule = { . . . prevRules } ;
const h = { . . . rule . Hosts [ hostIndex ] } ;
h . Paths [ pathIndex ] = {
. . . h . Paths [ pathIndex ] ,
[ key ] : key === 'ServicePort' ? Number ( val ) : val ,
} ;
// set the first port of the service as the default port
if (
key === 'ServiceName' &&
servicePorts [ val ] &&
servicePorts [ val ] . length > 0
) {
h . Paths [ pathIndex ] . ServicePort = Number ( servicePorts [ val ] [ 0 ] . value ) ;
}
rule . Hosts [ hostIndex ] = h ;
return rule ;
} ) ;
}
function handleAnnotationChange (
index : number ,
key : 'Key' | 'Value' ,
val : string
) {
setIngressRule ( ( prevRules ) = > {
const rules = { . . . prevRules } ;
rules . Annotations = rules . Annotations || [ ] ;
rules . Annotations [ index ] = rules . Annotations [ index ] || {
Key : '' ,
Value : '' ,
} ;
rules . Annotations [ index ] [ key ] = val ;
return rules ;
} ) ;
}
function addNewIngress ( namespace : string ) {
const newKey = ` ${ namespace } -ingress- ${
( ruleCounterByNamespace [ namespace ] || 0 ) + 1
} ` ;
const host : Host = {
Host : '' ,
Secret : '' ,
2023-06-19 02:11:50 +00:00
Paths : [ ] ,
2022-09-21 04:49:42 +00:00
Key : uuidv4 ( ) ,
} ;
const rule : Rule = {
Key : uuidv4 ( ) ,
Namespace : namespace ,
IngressName : newKey ,
2023-08-01 07:31:35 +00:00
IngressClassName : ingressRule.IngressClassName || '' ,
2023-08-16 06:07:46 +00:00
IngressType : ingressRule.IngressType || '' ,
2022-09-21 04:49:42 +00:00
Hosts : [ host ] ,
} ;
setIngressRule ( rule ) ;
}
function addNewIngressHost ( noHost = false ) {
const rule = { . . . ingressRule } ;
2023-06-21 21:33:22 +00:00
const path : Path = {
Key : uuidv4 ( ) ,
ServiceName : '' ,
ServicePort : 0 ,
Route : '' ,
PathType : 'Prefix' ,
} ;
2022-09-21 04:49:42 +00:00
const host : Host = {
Host : '' ,
Secret : '' ,
2023-06-21 21:33:22 +00:00
Paths : noHost ? [ path ] : [ ] ,
2022-09-21 04:49:42 +00:00
NoHost : noHost ,
Key : uuidv4 ( ) ,
} ;
rule . Hosts . push ( host ) ;
setIngressRule ( rule ) ;
}
function addNewIngressRoute ( hostIndex : number ) {
const rule = { . . . ingressRule } ;
const path : Path = {
ServiceName : '' ,
ServicePort : 0 ,
Route : '' ,
PathType : 'Prefix' ,
Key : uuidv4 ( ) ,
} ;
rule . Hosts [ hostIndex ] . Paths . push ( path ) ;
setIngressRule ( rule ) ;
}
2022-10-24 20:41:30 +00:00
function addNewAnnotation ( type ? : 'rewrite' | 'regex' | 'ingressClass' ) {
2022-09-21 04:49:42 +00:00
const rule = { . . . ingressRule } ;
const annotation : Annotation = {
Key : '' ,
Value : '' ,
ID : uuidv4 ( ) ,
} ;
2022-10-24 20:41:30 +00:00
switch ( type ) {
case 'rewrite' :
annotation . Key = 'nginx.ingress.kubernetes.io/rewrite-target' ;
annotation . Value = '/$1' ;
break ;
case 'regex' :
annotation . Key = 'nginx.ingress.kubernetes.io/use-regex' ;
annotation . Value = 'true' ;
break ;
case 'ingressClass' :
annotation . Key = 'kubernetes.io/ingress.class' ;
annotation . Value = '' ;
break ;
default :
break ;
2022-09-21 04:49:42 +00:00
}
rule . Annotations = rule . Annotations || [ ] ;
rule . Annotations ? . push ( annotation ) ;
setIngressRule ( rule ) ;
}
function removeAnnotation ( index : number ) {
const rule = { . . . ingressRule } ;
if ( index > - 1 ) {
rule . Annotations ? . splice ( index , 1 ) ;
}
setIngressRule ( rule ) ;
}
function removeIngressRoute ( hostIndex : number , pathIndex : number ) {
const rule = { . . . ingressRule , Hosts : [ . . . ingressRule . Hosts ] } ;
if ( hostIndex > - 1 && pathIndex > - 1 ) {
rule . Hosts [ hostIndex ] . Paths . splice ( pathIndex , 1 ) ;
}
setIngressRule ( rule ) ;
}
function removeIngressHost ( hostIndex : number ) {
const rule = { . . . ingressRule , Hosts : [ . . . ingressRule . Hosts ] } ;
if ( hostIndex > - 1 ) {
rule . Hosts . splice ( hostIndex , 1 ) ;
}
setIngressRule ( rule ) ;
}
function reloadTLSCerts() {
configResults . refetch ( ) ;
}
function handleCreateIngressRules() {
const rule = { . . . ingressRule } ;
2022-10-24 20:41:30 +00:00
const classNameToSend =
rule . IngressClassName === 'none' ? '' : rule . IngressClassName ;
2022-09-21 04:49:42 +00:00
const ingress : Ingress = {
Namespace : namespace ,
Name : rule.IngressName ,
2022-10-24 20:41:30 +00:00
ClassName : classNameToSend ,
2022-09-21 04:49:42 +00:00
Hosts : rule.Hosts.map ( ( host ) = > host . Host ) ,
Paths : preparePaths ( rule . IngressName , rule . Hosts ) ,
TLS : prepareTLS ( rule . Hosts ) ,
Annotations : prepareAnnotations ( rule . Annotations || [ ] ) ,
} ;
if ( isEdit ) {
updateIngressMutation . mutate (
{ environmentId , ingress } ,
{
onSuccess : ( ) = > {
notifySuccess ( 'Success' , 'Ingress updated successfully' ) ;
router . stateService . go ( 'kubernetes.ingresses' ) ;
} ,
}
) ;
} else {
createIngressMutation . mutate (
{ environmentId , ingress } ,
{
onSuccess : ( ) = > {
notifySuccess ( 'Success' , 'Ingress created successfully' ) ;
router . stateService . go ( 'kubernetes.ingresses' ) ;
} ,
}
) ;
}
}
}