feat(docker-json): make internal port optional, add extra_hosts, network_mode and ulimits (#1631)

This commit is contained in:
Nicolas Meienberger 2024-08-25 12:19:11 +02:00 committed by GitHub
parent 7d1b522f1f
commit 2e0a8839e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 170 additions and 7 deletions

View File

@ -12,7 +12,6 @@ describe('getDockerCompose', async () => {
const serviceImage2 = faker.system.semver();
const servicePort1 = faker.number.int({ min: 64, max: 65535 });
const servicePort2 = faker.number.int({ min: 64, max: 65535 });
const fakeEnv = {
one: faker.system.semver(),
@ -46,7 +45,6 @@ describe('getDockerCompose', async () => {
},
dependsOn: [serviceName1],
image: serviceImage2,
internalPort: servicePort2,
addPorts: [
{ containerPort: 3000, hostPort: 4444, tcp: true },
{ containerPort: 3001, hostPort: 4445, udp: true },
@ -216,4 +214,106 @@ describe('getDockerCompose', async () => {
"
`);
});
it('should remove port mappings and network if networkMode is set', async () => {
// arrange
const serviceName1 = faker.word.noun();
const serviceImage1 = faker.system.semver();
const servicePort1 = faker.number.int({ min: 64, max: 65535 });
const services = [
{
isMain: true,
name: serviceName1,
image: serviceImage1,
internalPort: servicePort1,
networkMode: 'host',
},
] satisfies ServiceInput[];
// act
const result = getDockerCompose(services, {
exposed: false,
exposedLocal: false,
openPort: false,
isVisibleOnGuestDashboard: false,
});
// assert
expect(result).toMatchInlineSnapshot(`
"services:
${serviceName1}:
image: ${serviceImage1}
container_name: ${serviceName1}
restart: unless-stopped
network_mode: host
labels:
generated: true
traefik.enable: false
traefik.http.middlewares.${serviceName1}-web-redirect.redirectscheme.scheme: https
traefik.http.services.${serviceName1}.loadbalancer.server.port: "${servicePort1}"
networks:
tipi_main_network:
name: runtipi_tipi_main_network
external: true
"
`);
});
it('can add ulimits to service', async () => {
// arrange
const serviceName1 = faker.word.noun();
const serviceImage1 = faker.system.semver();
const servicePort1 = faker.number.int({ min: 64, max: 65535 });
const services = [
{
isMain: true,
name: serviceName1,
image: serviceImage1,
internalPort: servicePort1,
ulimits: {
nofile: 1000,
nproc: {
soft: 1000,
hard: 1000,
},
},
},
] satisfies ServiceInput[];
// act
const result = getDockerCompose(services, {
exposed: false,
exposedLocal: false,
openPort: false,
isVisibleOnGuestDashboard: false,
});
// assert
expect(result).toMatchInlineSnapshot(`
"services:
${serviceName1}:
image: ${serviceImage1}
container_name: ${serviceName1}
restart: unless-stopped
networks:
- tipi_main_network
ulimits:
nproc:
soft: 1000
hard: 1000
nofile: 1000
labels:
generated: true
traefik.enable: false
traefik.http.middlewares.${serviceName1}-web-redirect.redirectscheme.scheme: https
traefik.http.services.${serviceName1}.loadbalancer.server.port: "${servicePort1}"
networks:
tipi_main_network:
name: runtipi_tipi_main_network
external: true
"
`);
});
});

View File

@ -17,12 +17,19 @@ const buildService = (params: Service, form: AppEventForm) => {
.setCommand(params.command)
.setHealthCheck(params.healthCheck)
.setDependsOn(params.dependsOn)
.addPorts(params.addPorts)
.addVolumes(params.volumes)
.setRestartPolicy('unless-stopped')
.addNetwork('tipi_main_network');
.addExtraHosts(params.extraHosts)
.addUlimits(params.ulimits)
.addPorts(params.addPorts)
.addNetwork('tipi_main_network')
.setNetworkMode(params.networkMode);
if (params.isMain) {
if (!params.internalPort) {
throw new Error('Main service must have an internal port specified');
}
if (form.openPort) {
service.addPort({
containerPort: params.internalPort,

View File

@ -1 +1,3 @@
export * from './constants';
import { APP_DIR, DATA_DIR, APP_DATA_DIR } from './constants';
export { APP_DIR, DATA_DIR, APP_DATA_DIR };

View File

@ -10,11 +10,19 @@ const dependsOnSchema = z.union([
),
]);
const ulimitsSchema = z.object({
nproc: z.number().or(z.object({ soft: z.number(), hard: z.number() })),
nofile: z.number().or(z.object({ soft: z.number(), hard: z.number() })),
});
export const serviceSchema = z.object({
image: z.string(),
name: z.string(),
internalPort: z.number(),
internalPort: z.number().optional(),
isMain: z.boolean().optional(),
networkMode: z.string().optional(),
extraHosts: z.array(z.string()).optional(),
ulimits: ulimitsSchema.optional(),
addPorts: z
.array(
z.object({

View File

@ -20,6 +20,11 @@ interface HealthCheck {
retries: number;
}
interface Ulimits {
nproc: number | { soft: number; hard: number };
nofile: number | { soft: number; hard: number };
}
export interface BuilderService {
image: string;
containerName: string;
@ -32,6 +37,9 @@ export interface BuilderService {
labels?: Record<string, string | boolean>;
dependsOn?: DependsOn;
networks?: string[];
networkMode?: string;
extraHosts?: string[];
ulimits?: Ulimits;
}
export type BuiltService = ReturnType<typeof ServiceBuilder.prototype.build>;
@ -288,6 +296,34 @@ export class ServiceBuilder {
return this;
}
setNetworkMode(networkMode?: string) {
if (!networkMode) {
return this;
}
this.service.networkMode = networkMode;
return this;
}
addExtraHosts(extraHosts?: string[]) {
if (!extraHosts) {
return this;
}
this.service.extraHosts = extraHosts;
return this;
}
addUlimits(ulimits?: Ulimits) {
if (!ulimits) {
return this;
}
this.service.ulimits = ulimits;
return this;
}
/**
* Builds the service object.
* @returns The built service object.
@ -306,11 +342,19 @@ export class ServiceBuilder {
throw new Error('Service name and image are required');
}
if (this.service.networkMode) {
this.service.ports = undefined;
this.service.networks = undefined;
}
return {
image: this.service.image,
container_name: this.service.containerName,
restart: this.service.restart,
networks: this.service.networks,
network_mode: this.service.networkMode,
extra_hosts: this.service.extraHosts,
ulimits: this.service.ulimits,
healthcheck: this.service.healthCheck,
environment: this.service.environment,
ports: this.service.ports,

View File

@ -1 +1,3 @@
export * from './docker-helpers';
import { compose, handleViewAppLogsEvent, handleViewRuntipiLogsEvent } from './docker-helpers';
export { compose, handleViewAppLogsEvent, handleViewRuntipiLogsEvent };