Add TCP Healthcheck
This commit is contained in:
parent
d1ab6ed489
commit
8392503df7
37 changed files with 2416 additions and 307 deletions
|
|
@ -33,8 +33,18 @@ describe('<TcpServicePage />', () => {
|
|||
address: 'http://10.0.1.12:80',
|
||||
},
|
||||
],
|
||||
passHostHeader: true,
|
||||
terminationDelay: 10,
|
||||
healthCheck: {
|
||||
interval: '30s',
|
||||
timeout: '10s',
|
||||
port: 8080,
|
||||
unhealthyInterval: '1m',
|
||||
send: 'PING',
|
||||
expect: 'PONG',
|
||||
},
|
||||
},
|
||||
serverStatus: {
|
||||
'http://10.0.1.12:80': 'UP',
|
||||
},
|
||||
status: 'enabled',
|
||||
usedBy: ['router-test1@docker'],
|
||||
|
|
@ -65,19 +75,31 @@ describe('<TcpServicePage />', () => {
|
|||
const titleTags = headings.filter((h1) => h1.innerHTML === 'service-test1')
|
||||
expect(titleTags.length).toBe(1)
|
||||
|
||||
const serviceDetails = getByTestId('service-details')
|
||||
const serviceDetails = getByTestId('tcp-service-details')
|
||||
expect(serviceDetails.innerHTML).toContain('Type')
|
||||
expect(serviceDetails.innerHTML).toContain('loadbalancer')
|
||||
expect(serviceDetails.innerHTML).toContain('Provider')
|
||||
expect(serviceDetails.querySelector('svg[data-testid="docker"]')).toBeTruthy()
|
||||
expect(serviceDetails.innerHTML).toContain('Status')
|
||||
expect(serviceDetails.innerHTML).toContain('Success')
|
||||
expect(serviceDetails.innerHTML).toContain('Pass Host Header')
|
||||
expect(serviceDetails.innerHTML).toContain('True')
|
||||
expect(serviceDetails.innerHTML).toContain('Termination Delay')
|
||||
expect(serviceDetails.innerHTML).toContain('10 ms')
|
||||
|
||||
const serversList = getByTestId('servers-list')
|
||||
const healthCheck = getByTestId('tcp-health-check')
|
||||
expect(healthCheck.innerHTML).toContain('Interval')
|
||||
expect(healthCheck.innerHTML).toContain('30s')
|
||||
expect(healthCheck.innerHTML).toContain('Timeout')
|
||||
expect(healthCheck.innerHTML).toContain('10s')
|
||||
expect(healthCheck.innerHTML).toContain('Port')
|
||||
expect(healthCheck.innerHTML).toContain('8080')
|
||||
expect(healthCheck.innerHTML).toContain('Unhealthy Interval')
|
||||
expect(healthCheck.innerHTML).toContain('1m')
|
||||
expect(healthCheck.innerHTML).toContain('Send')
|
||||
expect(healthCheck.innerHTML).toContain('PING')
|
||||
expect(healthCheck.innerHTML).toContain('Expect')
|
||||
expect(healthCheck.innerHTML).toContain('PONG')
|
||||
|
||||
const serversList = getByTestId('tcp-servers-list')
|
||||
expect(serversList.childNodes.length).toBe(1)
|
||||
expect(serversList.innerHTML).toContain('http://10.0.1.12:80')
|
||||
|
||||
|
|
@ -130,7 +152,7 @@ describe('<TcpServicePage />', () => {
|
|||
<TcpServiceRender name="mock-service" data={mockData as any} error={undefined} />,
|
||||
)
|
||||
|
||||
const serversList = getByTestId('servers-list')
|
||||
const serversList = getByTestId('tcp-servers-list')
|
||||
expect(serversList.childNodes.length).toBe(1)
|
||||
expect(serversList.innerHTML).toContain('http://10.0.1.12:81')
|
||||
|
||||
|
|
@ -160,4 +182,62 @@ describe('<TcpServicePage />', () => {
|
|||
getByTestId('routers-table')
|
||||
}).toThrow('Unable to find an element by: [data-testid="routers-table"]')
|
||||
})
|
||||
|
||||
it('should render weighted services', async () => {
|
||||
const mockData = {
|
||||
weighted: {
|
||||
services: [
|
||||
{
|
||||
name: 'service1@docker',
|
||||
weight: 80,
|
||||
},
|
||||
{
|
||||
name: 'service2@kubernetes',
|
||||
weight: 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 'enabled',
|
||||
usedBy: ['router-test1@docker'],
|
||||
name: 'weighted-service-test',
|
||||
provider: 'docker',
|
||||
type: 'weighted',
|
||||
routers: [
|
||||
{
|
||||
entryPoints: ['tcp'],
|
||||
service: 'weighted-service-test',
|
||||
rule: 'HostSNI(`*`)',
|
||||
status: 'enabled',
|
||||
using: ['tcp'],
|
||||
name: 'router-test1@docker',
|
||||
provider: 'docker',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const { container, getByTestId } = renderWithProviders(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
<TcpServiceRender name="mock-service" data={mockData as any} error={undefined} />,
|
||||
)
|
||||
|
||||
const headings = Array.from(container.getElementsByTagName('h1'))
|
||||
const titleTags = headings.filter((h1) => h1.innerHTML === 'weighted-service-test')
|
||||
expect(titleTags.length).toBe(1)
|
||||
|
||||
const serviceDetails = getByTestId('tcp-service-details')
|
||||
expect(serviceDetails.innerHTML).toContain('Type')
|
||||
expect(serviceDetails.innerHTML).toContain('weighted')
|
||||
expect(serviceDetails.innerHTML).toContain('Provider')
|
||||
expect(serviceDetails.querySelector('svg[data-testid="docker"]')).toBeTruthy()
|
||||
expect(serviceDetails.innerHTML).toContain('Status')
|
||||
expect(serviceDetails.innerHTML).toContain('Success')
|
||||
|
||||
const weightedServices = getByTestId('tcp-weighted-services')
|
||||
expect(weightedServices.childNodes.length).toBe(2)
|
||||
expect(weightedServices.innerHTML).toContain('service1@docker')
|
||||
expect(weightedServices.innerHTML).toContain('80')
|
||||
expect(weightedServices.innerHTML).toContain('service2@kubernetes')
|
||||
expect(weightedServices.innerHTML).toContain('20')
|
||||
expect(weightedServices.querySelector('svg[data-testid="docker"]')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,19 +1,234 @@
|
|||
import { Flex, H1, Skeleton, styled, Text } from '@traefiklabs/faency'
|
||||
import { Box, Flex, H1, Skeleton, styled, Text } from '@traefiklabs/faency'
|
||||
import { useMemo } from 'react'
|
||||
import { FiGlobe, FiInfo, FiShield } from 'react-icons/fi'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import { DetailSectionSkeleton } from 'components/resources/DetailSections'
|
||||
import ProviderIcon from 'components/icons/providers'
|
||||
import {
|
||||
DetailSection,
|
||||
DetailSectionSkeleton,
|
||||
ItemBlock,
|
||||
ItemTitle,
|
||||
LayoutTwoCols,
|
||||
ProviderName,
|
||||
} from 'components/resources/DetailSections'
|
||||
import { ResourceStatus } from 'components/resources/ResourceStatus'
|
||||
import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection'
|
||||
import { ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { ResourceDetailDataType, ServiceDetailType, useResourceDetail } from 'hooks/use-resource-detail'
|
||||
import Page from 'layout/Page'
|
||||
import { ServicePanels } from 'pages/http/HttpService'
|
||||
import { NotFound } from 'pages/NotFound'
|
||||
|
||||
type TcpDetailProps = {
|
||||
data: ServiceDetailType
|
||||
}
|
||||
|
||||
const SpacedColumns = styled(Flex, {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
|
||||
gridGap: '16px',
|
||||
})
|
||||
|
||||
const ServicesGrid = styled(Box, {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '2fr 1fr 1fr',
|
||||
alignItems: 'center',
|
||||
padding: '$3 $5',
|
||||
borderBottom: '1px solid $tableRowBorder',
|
||||
})
|
||||
|
||||
const ServersGrid = styled(Box, {
|
||||
display: 'grid',
|
||||
alignItems: 'center',
|
||||
padding: '$3 $5',
|
||||
borderBottom: '1px solid $tableRowBorder',
|
||||
})
|
||||
|
||||
const GridTitle = styled(Text, {
|
||||
fontSize: '14px',
|
||||
fontWeight: 700,
|
||||
color: 'hsl(0, 0%, 56%)',
|
||||
})
|
||||
|
||||
type TcpServer = {
|
||||
address: string
|
||||
}
|
||||
|
||||
type ServerStatus = {
|
||||
[server: string]: string
|
||||
}
|
||||
|
||||
type TcpHealthCheck = {
|
||||
port?: number
|
||||
send?: string
|
||||
expect?: string
|
||||
interval?: string
|
||||
unhealthyInterval?: string
|
||||
timeout?: string
|
||||
}
|
||||
|
||||
function getTcpServerStatusList(data: ServiceDetailType): ServerStatus {
|
||||
const serversList: ServerStatus = {}
|
||||
|
||||
data.loadBalancer?.servers?.forEach((server: any) => {
|
||||
// TCP servers should have address, but handle both url and address for compatibility
|
||||
const serverKey = (server as TcpServer).address || (server as any).url
|
||||
if (serverKey) {
|
||||
serversList[serverKey] = 'DOWN'
|
||||
}
|
||||
})
|
||||
|
||||
if (data.serverStatus) {
|
||||
Object.entries(data.serverStatus).forEach(([server, status]) => {
|
||||
serversList[server] = status
|
||||
})
|
||||
}
|
||||
|
||||
return serversList
|
||||
}
|
||||
|
||||
export const TcpServicePanels = ({ data }: TcpDetailProps) => {
|
||||
const serversList = getTcpServerStatusList(data)
|
||||
const getProviderFromName = (serviceName: string): string => {
|
||||
const [, provider] = serviceName.split('@')
|
||||
return provider || data.provider
|
||||
}
|
||||
const providerName = useMemo(() => {
|
||||
return data.provider
|
||||
}, [data.provider])
|
||||
|
||||
return (
|
||||
<SpacedColumns css={{ mb: '$5', pb: '$5' }} data-testid="tcp-service-details">
|
||||
<DetailSection narrow icon={<FiInfo size={20} />} title="Service Details">
|
||||
<LayoutTwoCols>
|
||||
{data.type && (
|
||||
<ItemBlock title="Type">
|
||||
<Text css={{ lineHeight: '32px' }}>{data.type}</Text>
|
||||
</ItemBlock>
|
||||
)}
|
||||
{data.provider && (
|
||||
<ItemBlock title="Provider">
|
||||
<ProviderIcon name={data.provider} />
|
||||
<ProviderName css={{ ml: '$2' }}>{providerName}</ProviderName>
|
||||
</ItemBlock>
|
||||
)}
|
||||
</LayoutTwoCols>
|
||||
{data.status && (
|
||||
<ItemBlock title="Status">
|
||||
<ResourceStatus status={data.status} withLabel />
|
||||
</ItemBlock>
|
||||
)}
|
||||
{data.loadBalancer && (
|
||||
<>
|
||||
{data.loadBalancer.terminationDelay && (
|
||||
<ItemBlock title="Termination Delay">
|
||||
<Text>{`${data.loadBalancer.terminationDelay} ms`}</Text>
|
||||
</ItemBlock>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DetailSection>
|
||||
{data.loadBalancer?.healthCheck && (
|
||||
<DetailSection narrow icon={<FiShield size={20} />} title="Health Check">
|
||||
<Box data-testid="tcp-health-check">
|
||||
{(() => {
|
||||
const tcpHealthCheck = data.loadBalancer.healthCheck as unknown as TcpHealthCheck
|
||||
return (
|
||||
<>
|
||||
<LayoutTwoCols>
|
||||
{tcpHealthCheck.interval && (
|
||||
<ItemBlock title="Interval">
|
||||
<Text>{tcpHealthCheck.interval}</Text>
|
||||
</ItemBlock>
|
||||
)}
|
||||
{tcpHealthCheck.timeout && (
|
||||
<ItemBlock title="Timeout">
|
||||
<Text>{tcpHealthCheck.timeout}</Text>
|
||||
</ItemBlock>
|
||||
)}
|
||||
</LayoutTwoCols>
|
||||
<LayoutTwoCols>
|
||||
{tcpHealthCheck.port && (
|
||||
<ItemBlock title="Port">
|
||||
<Text>{tcpHealthCheck.port}</Text>
|
||||
</ItemBlock>
|
||||
)}
|
||||
{tcpHealthCheck.unhealthyInterval && (
|
||||
<ItemBlock title="Unhealthy Interval">
|
||||
<Text>{tcpHealthCheck.unhealthyInterval}</Text>
|
||||
</ItemBlock>
|
||||
)}
|
||||
</LayoutTwoCols>
|
||||
<LayoutTwoCols>
|
||||
{tcpHealthCheck.send && (
|
||||
<ItemBlock title="Send">
|
||||
<Tooltip label={tcpHealthCheck.send} action="copy">
|
||||
<Text>{tcpHealthCheck.send}</Text>
|
||||
</Tooltip>
|
||||
</ItemBlock>
|
||||
)}
|
||||
{tcpHealthCheck.expect && (
|
||||
<ItemBlock title="Expect">
|
||||
<Tooltip label={tcpHealthCheck.expect} action="copy">
|
||||
<Text>{tcpHealthCheck.expect}</Text>
|
||||
</Tooltip>
|
||||
</ItemBlock>
|
||||
)}
|
||||
</LayoutTwoCols>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</Box>
|
||||
</DetailSection>
|
||||
)}
|
||||
{!!data?.weighted?.services?.length && (
|
||||
<DetailSection narrow icon={<FiGlobe size={20} />} title="Services" noPadding>
|
||||
<>
|
||||
<ServicesGrid css={{ mt: '$2' }}>
|
||||
<GridTitle>Name</GridTitle>
|
||||
<GridTitle css={{ textAlign: 'center' }}>Weight</GridTitle>
|
||||
<GridTitle css={{ textAlign: 'center' }}>Provider</GridTitle>
|
||||
</ServicesGrid>
|
||||
<Box data-testid="tcp-weighted-services">
|
||||
{data.weighted.services.map((service) => (
|
||||
<ServicesGrid key={service.name}>
|
||||
<Text>{service.name}</Text>
|
||||
<Text css={{ textAlign: 'center' }}>{service.weight}</Text>
|
||||
<Flex css={{ justifyContent: 'center' }}>
|
||||
<ProviderIcon name={getProviderFromName(service.name)} />
|
||||
</Flex>
|
||||
</ServicesGrid>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
</DetailSection>
|
||||
)}
|
||||
{Object.keys(serversList).length > 0 && (
|
||||
<DetailSection narrow icon={<FiGlobe size={20} />} title="Servers" noPadding>
|
||||
<>
|
||||
<ServersGrid css={{ gridTemplateColumns: '25% auto', mt: '$2' }}>
|
||||
<ItemTitle css={{ mb: 0 }}>Status</ItemTitle>
|
||||
<ItemTitle css={{ mb: 0 }}>Address</ItemTitle>
|
||||
</ServersGrid>
|
||||
<Box data-testid="tcp-servers-list">
|
||||
{Object.entries(serversList).map(([server, status]) => (
|
||||
<ServersGrid key={server} css={{ gridTemplateColumns: '25% auto' }}>
|
||||
<ResourceStatus status={status === 'UP' ? 'enabled' : 'disabled'} />
|
||||
<Box>
|
||||
<Tooltip label={server} action="copy">
|
||||
<Text>{server}</Text>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</ServersGrid>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
</DetailSection>
|
||||
)}
|
||||
</SpacedColumns>
|
||||
)
|
||||
}
|
||||
|
||||
type TcpServiceRenderProps = {
|
||||
data?: ResourceDetailDataType
|
||||
error?: Error
|
||||
|
|
@ -38,6 +253,7 @@ export const TcpServiceRender = ({ data, error, name }: TcpServiceRenderProps) =
|
|||
<SpacedColumns>
|
||||
<DetailSectionSkeleton narrow />
|
||||
<DetailSectionSkeleton narrow />
|
||||
<DetailSectionSkeleton narrow />
|
||||
</SpacedColumns>
|
||||
<UsedByRoutersSkeleton />
|
||||
</Page>
|
||||
|
|
@ -51,7 +267,7 @@ export const TcpServiceRender = ({ data, error, name }: TcpServiceRenderProps) =
|
|||
return (
|
||||
<Page title={name}>
|
||||
<H1 css={{ mb: '$7' }}>{data.name}</H1>
|
||||
<ServicePanels data={data} />
|
||||
<TcpServicePanels data={data} />
|
||||
<UsedByRoutersSection data={data} protocol="tcp" />
|
||||
</Page>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue