Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-8440 Remove devDependencies from docker image #5419

Merged
merged 30 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
19fb166
Fix comment in esmodules-bundler
dyedwiper Jan 7, 2025
2b35c75
Remove empty file test.ts
dyedwiper Jan 7, 2025
e3b9b33
Move fishery from devDependencies to dependencies
dyedwiper Jan 7, 2025
a56d531
Copy factories from testing to management module
dyedwiper Jan 7, 2025
c44e7d0
Remove any imports from testing folders in production code
dyedwiper Jan 7, 2025
4002902
Exclude testing folders from build
dyedwiper Jan 7, 2025
af07dc5
Add pruning of dev dependencies in Dockerfile
dyedwiper Jan 7, 2025
a359451
Update commands in Dockerfile
dyedwiper Jan 7, 2025
4ce6e08
Fix comment in Dockerfile
dyedwiper Jan 9, 2025
f8baf47
Simplify some paths in Dockerfile
dyedwiper Jan 9, 2025
5a305d9
Add blank line at end of tsconfig.app.json
dyedwiper Jan 9, 2025
499964c
Move all npm commands in single RUN instruction
dyedwiper Jan 10, 2025
365a015
Update Dockerfile to use multi-stage build
dyedwiper Jan 10, 2025
e442a57
Fix copying of files in Dockerfile
dyedwiper Jan 10, 2025
036e93f
Remove cleaning of cache in first stage of Dockerfile
dyedwiper Jan 13, 2025
0a480e5
Merge branch 'main' into BC-8440-prune-dev-deps
dyedwiper Jan 13, 2025
d747072
Merge branch 'main' into BC-8440-prune-dev-deps
dyedwiper Jan 16, 2025
f4a3de8
Order dependencies in package.json alphabetically
dyedwiper Jan 16, 2025
3bfe363
Fix imports overlooked in merge
dyedwiper Jan 16, 2025
c80b292
Conigure Sonarcloud to ignore duplication in seed-data factories
dyedwiper Jan 16, 2025
af558ed
Fix path in sonar config
dyedwiper Jan 16, 2025
a6ce294
Update sonar exclusions
dyedwiper Jan 16, 2025
b38627b
Merge branch 'main' into BC-8440-prune-dev-deps
dyedwiper Jan 16, 2025
1223c32
Remove forgotten comma in sonar config
dyedwiper Jan 16, 2025
dbc68c3
Merge branch 'BC-8440-prune-dev-deps' of github.com:hpi-schul-cloud/s…
dyedwiper Jan 16, 2025
692f3fa
Fix copying of serverversion in Dockerfile
dyedwiper Jan 16, 2025
ff6d72e
Fix copying of serverversion in Dockerfile once more
dyedwiper Jan 16, 2025
cfe4137
Merge branch 'main' into BC-8440-prune-dev-deps
dyedwiper Jan 17, 2025
bc25838
Merge branch 'main' into BC-8440-prune-dev-deps
dyedwiper Jan 17, 2025
05b10f7
Fix copying of serverversion in Dockerfile once more
dyedwiper Jan 17, 2025
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
56 changes: 38 additions & 18 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,27 +1,47 @@
FROM docker.io/node:22 AS git
FROM docker.io/node:22-alpine AS builder

RUN mkdir /app && chown -R node:node /app
WORKDIR /app
COPY .git .
RUN git config --global --add safe.directory /app && echo "{\"sha\": \"$(git rev-parse HEAD)\", \"version\": \"$(git describe --tags --abbrev=0)\", \"commitDate\": \"$(git log -1 --format=%cd --date=format:'%Y-%m-%dT%H:%M:%SZ')\", \"birthdate\": \"$(date +%Y-%m-%dT%H:%M:%SZ)\"}" > /app/serverversion
RUN apk add --no-cache git
COPY .git ./.git

RUN git config --global --add safe.directory /app \
&& echo "{\"sha\": \"$(git rev-parse HEAD)\", \"version\": \"$(git describe --tags --abbrev=0)\", \"commitDate\": \"$(git log -1 --format=%cd --date=format:'%Y-%m-%dT%H:%M:%SZ')\", \"birthdate\": \"$(date +%Y-%m-%dT%H:%M:%SZ)\"}" > /app/serverversion

COPY package.json package-lock.json tsconfig.json tsconfig.build.json nest-cli.json ./
COPY apps apps
COPY config config
COPY esbuild esbuild
COPY src src

RUN npm ci && npm run build


FROM docker.io/node:22-alpine

ENV TZ=Europe/Berlin
RUN apk add --no-cache git make python3
# to run ldap sync as script curl is needed
RUN apk add --no-cache curl
RUN apk add --no-cache git make python3 curl

WORKDIR /schulcloud-server
COPY tsconfig.json tsconfig.build.json package.json package-lock.json .eslintrc.js .eslintignore nest-cli.json ./
COPY esbuild ./esbuild
RUN npm ci && npm cache clean --force
COPY config /schulcloud-server/config
COPY backup /schulcloud-server/backup
COPY src /schulcloud-server/src
COPY apps /schulcloud-server/apps
COPY --from=git /app/serverversion /schulcloud-server/apps/server/static-assets
COPY scripts/ldapSync.sh /schulcloud-server/scripts/
RUN npm run build

COPY package.json package-lock.json ./
COPY backup backup
COPY config config
COPY scripts/ldapSync.sh scripts/
COPY src src

COPY --from=builder /app/dist dist
COPY --from=builder /app/apps/server/static-assets apps/server/static-assets

# The postinstall script must be disabled, because esbuild is a dev dependency and not installed here.
RUN npm pkg delete scripts.postinstall \
&& npm ci --omit=dev \
&& npm cache clean --force

# The modules transpiled by esbuild need to be copied manually from the build stage.
COPY --from=builder /app/node_modules/@keycloak/keycloak-admin-client-cjs node_modules/@keycloak/keycloak-admin-client-cjs
COPY --from=builder /app/node_modules/file-type-cjs node_modules/file-type-cjs

ENV NODE_ENV=production
ENV NO_COLOR="true"
CMD npm run start

CMD npm start
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Action, AuthorizationService } from '@modules/authorization';
import { ExternalTool } from '@modules/tool/external-tool/domain';
import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain';
import { schoolExternalToolFactory } from '@modules/tool/school-external-tool/testing';
import { MediaUserLicense, mediaUserLicenseFactory, MediaUserLicenseService } from '@modules/user-license';
import { MediaUserLicense, MediaUserLicenseService } from '@modules/user-license';
import { mediaUserLicenseFactory } from '@modules/user-license/testing';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception';
Expand Down
164 changes: 164 additions & 0 deletions apps/server/src/modules/management/seed-data/factory/base.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { ObjectId } from '@mikro-orm/mongodb';
import type { EntityId } from '@shared/domain/types';
import { BuildOptions, DeepPartial, Factory, GeneratorFn, HookFn } from 'fishery';

/**
* Entity factory based on thoughtbot/fishery
* https://github.com/thoughtbot/fishery
*
* @template T The entity to be built
* @template U The properties interface of the entity
* @template I The transient parameters that your factory supports
* @template C The class of the factory object being created.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class BaseFactory<T, U, I = any, C = U> {
protected readonly propsFactory: Factory<U, I, C>;

constructor(private readonly EntityClass: { new (props: U): T }, propsFactory: Factory<U, I, C>) {
this.propsFactory = propsFactory;
}

/**
* Define a factory
* @template T The entity to be built
* @template U The properties interface of the entity
* @template I The transient parameters that your factory supports
* @template C The class of the factory object being created.
* @param EntityClass The constructor of the entity to be built.
* @param generator Your factory function - see `Factory.define()` in thoughtbot/fishery
* @returns
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static define<T, U, I = any, C = U, F = BaseFactory<T, U, I, C>>(
this: new (EntityClass: { new (props: U): T }, propsFactory: Factory<U, I, C>) => F,
EntityClass: { new (props: U): T },
generator: GeneratorFn<U, I, C>
): F {
const propsFactory = Factory.define<U, I, C>(generator);
const factory = new this(EntityClass, propsFactory);
return factory;
}

/**
* Build an entity using your factory
* @param params
* @returns an entity
*/
build(params?: DeepPartial<U>, options: BuildOptions<U, I> = {}): T {
const props = this.propsFactory.build(params, options);
const entity = new this.EntityClass(props);

return entity;
}

/**
* Build an entity using your factory and generate a id for it.
* @param params
* @param id
* @returns an entity
*/
buildWithId(params?: DeepPartial<U>, id?: string, options: BuildOptions<U, I> = {}): T {
const entity = this.build(params, options) as { _id: ObjectId; id: EntityId };
const generatedId = new ObjectId(id);
const entityWithId = Object.assign(entity, { _id: generatedId, id: generatedId.toHexString() });

return entityWithId as T;
}

/**
* Build a list of entities using your factory
* @param number
* @param params
* @returns a list of entities
*/
buildList(number: number, params?: DeepPartial<U>, options: BuildOptions<U, I> = {}): T[] {
const list: T[] = [];
for (let i = 0; i < number; i += 1) {
list.push(this.build(params, options));
}

return list;
}

buildListWithId(number: number, params?: DeepPartial<U>, options: BuildOptions<U, I> = {}): T[] {
const list: T[] = [];
for (let i = 0; i < number; i += 1) {
list.push(this.buildWithId(params, undefined, options));
}

return list;
}

/**
* Extend the factory by adding a function to be called after an object is built.
* @param afterBuildFn - the function to call. It accepts your object of type T. The value this function returns gets returned from "build"
* @returns a new factory
*/
afterBuild(afterBuildFn: HookFn<U>): this {
const newPropsFactory = this.propsFactory.afterBuild(afterBuildFn);
const newFactory = this.clone(newPropsFactory);

return newFactory;
}

/**
* Extend the factory by adding default associations to be passed to the factory when "build" is called
* @param associations
* @returns a new factory
*/
associations(associations: Partial<U>): this {
const newPropsFactory = this.propsFactory.associations(associations);
const newFactory = this.clone(newPropsFactory);

return newFactory;
}

/**
* Extend the factory by adding default parameters to be passed to the factory when "build" is called
* @param params
* @returns a new factory
*/
params(params: DeepPartial<U>): this {
const newPropsFactory = this.propsFactory.params(params);
const newFactory = this.clone(newPropsFactory);

return newFactory;
}

/**
* Extend the factory by adding default transient parameters to be passed to the factory when "build" is called
* @param transient - transient params
* @returns a new factory
*/
transient(transient: Partial<I>): this {
const newPropsFactory = this.propsFactory.transient(transient);
const newFactory = this.clone(newPropsFactory);

return newFactory;
}

/**
* Set sequence back to its default value
*/
rewindSequence(): void {
this.propsFactory.rewindSequence();
}

protected clone<F extends BaseFactory<T, U, I, C>>(this: F, propsFactory: Factory<U, I, C>): F {
const copy = new (this.constructor as {
new (EntityClass: { new (props: U): T }, propsOfFactory: Factory<U, I, C>): F;
})(this.EntityClass, propsFactory);

return copy;
}

/**
* Get the next sequence value
* @returns the next sequence value
*/
protected sequence(): number {
// eslint-disable-next-line @typescript-eslint/dot-notation
return this.propsFactory['sequence']();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CountyEmbeddable } from '@shared/domain/entity';
import { ObjectId } from '@mikro-orm/mongodb';
import { BaseFactory } from './base.factory';

export const countyEmbeddableFactory = BaseFactory.define<CountyEmbeddable, CountyEmbeddable>(
CountyEmbeddable,
({ sequence }) => {
const county = {
_id: new ObjectId(),
name: `County ${sequence}`,
countyId: sequence,
antaresKey: `antaresKey ${sequence}`,
};

return county;
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { FederalStateEntity, FederalStateProperties } from '@shared/domain/entity';
import { BaseFactory } from './base.factory';
import { countyEmbeddableFactory } from './county.embeddable.factory';

export const federalStateFactory = BaseFactory.define<FederalStateEntity, FederalStateProperties>(
FederalStateEntity,
() => {
return {
name: 'Hamburg',
abbreviation: 'HH',
logoUrl:
'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Coat_of_arms_of_Hamburg.svg/1200px-Coat_of_arms_of_Hamburg.svg.png',
counties: countyEmbeddableFactory.buildList(2),
createdAt: new Date(2020, 1),
updatedAt: new Date(2020, 1),
};
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Role, RoleProperties } from '@shared/domain/entity';
import { RoleName } from '@shared/domain/interface';
import { BaseFactory } from './base.factory';

export const roleFactory = BaseFactory.define<Role, RoleProperties>(Role, ({ sequence }) => {
return {
name: `role${sequence}` as unknown as RoleName,
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { SchoolEntity, SchoolProperties } from '@shared/domain/entity';
import { BaseFactory } from './base.factory';
import { federalStateFactory } from './federal-state.factory';
import { schoolYearFactory } from './schoolyear.factory';

export const schoolEntityFactory = BaseFactory.define<SchoolEntity, SchoolProperties>(SchoolEntity, ({ sequence }) => {
return {
name: `school #${sequence}`,
currentYear: schoolYearFactory.build(),
federalState: federalStateFactory.build(),
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { SchoolYearEntity, SchoolYearProperties } from '@shared/domain/entity/schoolyear.entity';
import { BaseFactory } from './base.factory';

type SchoolYearTransientParams = {
startYear: number;
};

class SchoolYearFactory extends BaseFactory<SchoolYearEntity, SchoolYearProperties, SchoolYearTransientParams> {
public withStartYear(startYear: number): this {
this.rewindSequence();
return this.transient({ startYear });
}
}

export const schoolYearFactory = SchoolYearFactory.define(SchoolYearEntity, ({ transientParams, sequence }) => {
const now = new Date();
const startYearWithoutSequence = transientParams?.startYear ?? now.getFullYear();
const sequenceStartingWithZero = sequence - 1;
let correction = 0;

if (now.getMonth() < 7 && !transientParams?.startYear) {
correction = 1;
}

const startYear = startYearWithoutSequence + sequenceStartingWithZero - correction;

const name = `${startYear}/${(startYear + 1).toString().slice(-2)}`;
const startDate = new Date(`${startYear}-08-01`);
const endDate = new Date(`${startYear + 1}-07-31`);

return { name, startDate, endDate };
});
Loading
Loading