Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/frontends.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ jobs:
modules: ${{ steps.set-matrix.outputs.modules }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
# paths-filter may need the merge-base; avoid its shallow-history deepen race.
fetch-depth: 0
- name: Verify shared frontend runtime settings
run: grep -qx 'ENV UV_USE_IO_URING=0' frontend/Dockerfile
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
Expand Down
19 changes: 16 additions & 3 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,28 @@ WORKDIR /app
FROM base AS deps

# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat && corepack enable && corepack prepare pnpm@8.9.0 --activate
RUN apk add --no-cache libc6-compat python3 make g++ && \
corepack enable && corepack prepare pnpm@8.9.0 --activate


# Install dependencies based on the preferred package manager root workspace
COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./

RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
[ -f pnpm-lock.yaml ] && pnpm fetch || \
(echo "Lockfile not found." && exit 1)
if [ ! -f pnpm-lock.yaml ]; then \
echo "Lockfile not found." >&2; \
exit 1; \
fi; \
for attempt in 1 2 3; do \
if pnpm fetch; then \
exit 0; \
fi; \
if [ "${attempt}" -lt 3 ]; then \
echo "pnpm fetch failed (attempt ${attempt}/3), retrying..." >&2; \
sleep $((attempt * 10)); \
fi; \
done; \
exit 1
COPY ./tsconfig.json ./tsconfig.json
COPY ./tsconfig.deps.json ./tsconfig.deps.json
COPY ./tsconfig.base.json ./tsconfig.base.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '@/utils/deployYaml2Json';
import type { AppEditType } from '@/types/app';
import { resolveAppImageName } from '@/utils/adapt';
import { rebindMainServiceRoutes } from '@/utils/network-routes';

const createApp = (customDomain = ''): AppEditType =>
({
Expand Down Expand Up @@ -208,6 +209,40 @@ describe('json2Ingress', () => {
]);
});

it('writes a regenerated main service name after stale route bindings are removed', () => {
const app = createApp();
app.networks[0].serviceName = '';
app.networks[0].port = 8080;
app.networks[0].routes = rebindMainServiceRoutes({
routes: [
{
path: '/',
pathType: 'Prefix',
serviceName: 'demo-old-service',
servicePort: 8080
},
{
path: '/api',
pathType: 'Prefix',
serviceName: 'demo-api',
servicePort: 8081
}
],
previousServiceName: 'demo-old-service'
});

const objects = yamlString2Objects(
json2Ingress(app, {
disableHttps: true
})
) as any[];
const paths = objects[0].spec.rules[0].http.paths;

expect(paths[0].backend.service.name).not.toBe('demo-old-service');
expect(paths[0].backend.service.name).toMatch(/^demo-[a-z]{12}$/);
expect(paths[1].backend.service.name).toBe('demo-api');
});

it('syncs the default main service route port with the network port', () => {
const app = createApp();
app.networks[0].port = 8080;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,67 @@
import { describe, expect, it } from 'vitest';
import { syncDefaultRouteServicePort } from '@/utils/network-routes';
import { rebindMainServiceRoutes, syncDefaultRouteServicePort } from '@/utils/network-routes';

describe('rebindMainServiceRoutes', () => {
it('removes a stale main service name while preserving explicit backends', () => {
expect(
rebindMainServiceRoutes({
routes: [
{
path: '/',
pathType: 'Prefix',
serviceName: 'demo-old-service',
servicePort: 80
},
{
path: '/healthz',
pathType: 'Exact',
serviceName: '',
servicePort: 80
},
{
path: '/api',
pathType: 'Prefix',
serviceName: 'demo-api',
servicePort: 8080
}
],
previousServiceName: 'demo-old-service'
})
).toEqual([
{
path: '/',
pathType: 'Prefix',
serviceName: '',
servicePort: 80
},
{
path: '/healthz',
pathType: 'Exact',
serviceName: '',
servicePort: 80
},
{
path: '/api',
pathType: 'Prefix',
serviceName: 'demo-api',
servicePort: 8080
}
]);
});

it('does not alter routes when the previous main service is unknown', () => {
const routes = [
{
path: '/api',
pathType: 'Prefix' as const,
serviceName: 'demo-api',
servicePort: 8080
}
];

expect(rebindMainServiceRoutes({ routes, previousServiceName: '' })).toBe(routes);
});
});

describe('syncDefaultRouteServicePort', () => {
it('updates the default main service route when the network port changes', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import type { AppEditType, ApplicationProtocolType } from '@/types/app';
import RouteRulesModal from './RouteRulesModal';
import { useCopyData } from '@/utils/tools';
import { buildExternalUrl, getExternalProtocol } from '@/utils/network-url';
import { syncDefaultRouteServicePort } from '@/utils/network-routes';
import { rebindMainServiceRoutes, syncDefaultRouteServicePort } from '@/utils/network-routes';
import type { CustomAccessModalParams } from './CustomAccessModal';
import type { CertificateCustomAccessModalParams } from './CertificateCustomAccessModal';
import dynamic from 'next/dynamic';
Expand Down Expand Up @@ -267,6 +267,17 @@ const withDefaultRoutes = (network: AppEditType['networks'][0]): AppEditType['ne
routes: network.routes?.length ? network.routes : [createDefaultRoute(network.port)]
});

const withoutMainServiceBinding = (
network: AppEditType['networks'][0]
): AppEditType['networks'][0] => ({
...network,
serviceName: '',
routes: rebindMainServiceRoutes({
routes: network.routes,
previousServiceName: network.serviceName
})
});

const getNextAvailablePort = (networks: AppEditType['networks']) => {
const usedPorts = new Set(networks.map((network) => Number(network.port)).filter(Boolean));

Expand Down Expand Up @@ -430,8 +441,7 @@ export function NetworkSection({
updateNetworks(
index,
withDefaultRoutes({
...currentNetwork,
serviceName: '',
...withoutMainServiceBinding(currentNetwork),
networkName: currentNetwork.networkName || `network-${nanoid()}`,
protocol: 'TCP',
appProtocol: currentNetwork.appProtocol || 'HTTP',
Expand All @@ -449,8 +459,7 @@ export function NetworkSection({
const { index } = action.payload;
clearPublicDomainErrorByIndex(index);
updateNetworks(index, {
...currentNetworks[index],
serviceName: '',
...withoutMainServiceBinding(currentNetworks[index]),
openPublicDomain: false,
openNodePort: false,
customDomain: '',
Expand All @@ -477,8 +486,7 @@ export function NetworkSection({
updateNetworks(
index,
withDefaultRoutes({
...currentNetwork,
serviceName: '',
...withoutMainServiceBinding(currentNetwork),
networkName: currentNetwork.networkName || `network-${nanoid()}`,
protocol: currentNetwork.appProtocol ? 'TCP' : currentNetwork.protocol,
appProtocol:
Expand Down Expand Up @@ -506,8 +514,7 @@ export function NetworkSection({
updateNetworks(
index,
withDefaultRoutes({
...currentNetwork,
serviceName: '',
...withoutMainServiceBinding(currentNetwork),
networkName: currentNetwork.networkName || `network-${nanoid()}`,
protocol: 'TCP',
appProtocol,
Expand All @@ -532,8 +539,7 @@ export function NetworkSection({
updateNetworks(
index,
withDefaultRoutes({
...currentNetwork,
serviceName: '',
...withoutMainServiceBinding(currentNetwork),
protocol: 'TCP',
appProtocol: protocol as any,
openNodePort: true,
Expand All @@ -549,8 +555,7 @@ export function NetworkSection({
updateNetworks(
index,
withDefaultRoutes({
...currentNetwork,
serviceName: '',
...withoutMainServiceBinding(currentNetwork),
protocol: 'TCP',
appProtocol: protocol as any,
openNodePort: false,
Expand All @@ -567,8 +572,7 @@ export function NetworkSection({
updateNetworks(
index,
withDefaultRoutes({
...currentNetwork,
serviceName: '',
...withoutMainServiceBinding(currentNetwork),
protocol: protocol as any,
appProtocol: undefined,
openNodePort: true,
Expand Down
37 changes: 23 additions & 14 deletions frontend/providers/applaunchpad/src/pages/app/edit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
validatePublicDomainPrefix
} from '@/utils/public-domain';
import { getCustomDomainBindings } from '@/utils/custom-domain';
import { rebindMainServiceRoutes } from '@/utils/network-routes';
import {
APP_NAME_BASE_MAX_LENGTH,
getInvalidNameMessageI18nKey
Expand Down Expand Up @@ -656,18 +657,12 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
});

if (Array.isArray(parsedData.networks)) {
const completeNetworks = parsedData.networks.map((network) => ({
networkName: network.networkName || `network-${nanoid()}`,
portName: network.portName || nanoid(),
port: network.port || 80,
protocol: network.protocol || 'TCP',
appProtocol: network.appProtocol || undefined,
openPublicDomain: network.openPublicDomain || false,
openNodePort: network.openNodePort || false,
publicDomain: network.publicDomain || nanoid(),
customDomain: network.customDomain || '',
domain: network.domain || SEALOS_DOMAIN,
routes: network.routes?.length
const currentNetworks = formHook.getValues('networks');
const completeNetworks = parsedData.networks.map((network) => {
const previousServiceName = currentNetworks.find(
(currentNetwork) => currentNetwork.portName === network.portName
)?.serviceName;
const routes = network.routes?.length
? network.routes.map((route) => ({
path: route.path || '/',
pathType: route.pathType || ('Prefix' as const),
Expand All @@ -681,8 +676,22 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
serviceName: '',
servicePort: network.port || 80
}
]
}));
];

return {
networkName: network.networkName || `network-${nanoid()}`,
portName: network.portName || nanoid(),
port: network.port || 80,
protocol: network.protocol || 'TCP',
appProtocol: network.appProtocol || undefined,
openPublicDomain: network.openPublicDomain || false,
openNodePort: network.openNodePort || false,
publicDomain: network.publicDomain || nanoid(),
customDomain: network.customDomain || '',
domain: network.domain || SEALOS_DOMAIN,
routes: rebindMainServiceRoutes({ routes, previousServiceName })
};
});
formHook.setValue('networks', completeNetworks);
}

Expand Down
27 changes: 27 additions & 0 deletions frontend/providers/applaunchpad/src/utils/network-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@ import type { AppNetworkRouteType } from '@/types/app';
const targetsMainService = (route: AppNetworkRouteType, networkServiceName?: string) =>
!route.serviceName || (!!networkServiceName && route.serviceName === networkServiceName);

export const rebindMainServiceRoutes = ({
routes,
previousServiceName
}: {
routes?: AppNetworkRouteType[];
previousServiceName?: string;
}) => {
if (!routes?.length || !previousServiceName) {
return routes;
}

let changed = false;
const nextRoutes = routes.map((route) => {
if (route.serviceName !== previousServiceName) {
return route;
}

changed = true;
return {
...route,
serviceName: ''
};
});

return changed ? nextRoutes : routes;
};

export const syncDefaultRouteServicePort = ({
routes,
previousPort,
Expand Down
Loading