UNPKG

gen-jhipster

Version:

VHipster - Spring Boot + Angular/React/Vue in one handy generator

356 lines (355 loc) 20.2 kB
/** * Copyright 2013-2026 the original author or authors from the JHipster project. * * This file is part of the JHipster project, see https://www.jhipster.tech/ * for more information. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import assert from 'node:assert'; import { existsSync } from 'node:fs'; import pathjs from 'node:path'; import chalk from 'chalk'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; import { monitoringTypes, serviceDiscoveryTypes } from "../../lib/jhipster/index.js"; import { createBase64Secret, stringHashCode } from "../../lib/utils/index.js"; import { normalizePath as normalize } from "../../lib/utils/path.js"; import { createFaker } from "../base-application/support/index.js"; import BaseWorkspacesGenerator from "../base-workspaces/index.js"; import { checkDocker } from "../base-workspaces/internal/docker-base.js"; import { askForApisixGateway, askForClustersModeWorkspace, askForMonitoring, askForServiceDiscoveryWorkspace, } from "../base-workspaces/internal/docker-prompts.js"; import { loadDockerDependenciesTask, loadDockerElasticsearchVersion } from "../base-workspaces/internal/index.js"; import { askForDirectoryPath } from "../base-workspaces/prompts.js"; import cleanupOldFilesTask from "./cleanup.js"; import { writeFiles } from "./files.js"; const { PROMETHEUS } = monitoringTypes; const { EUREKA, NO: NO_SERVICE_DISCOVERY } = serviceDiscoveryTypes; export default class DockerComposeGenerator extends BaseWorkspacesGenerator { async beforeQueue() { if (!this.fromBlueprint) { this.jhipsterConfig.deploymentType ??= 'docker-compose'; assert.equal(this.jhipsterConfig.deploymentType, 'docker-compose', 'Deployment type must be docker-compose'); await this.composeWithBlueprints(); } await this.dependsOnBootstrap('base-workspaces'); } get initializing() { return this.asInitializingTaskGroup({ sayHello() { this.log.log(chalk.white(`${chalk.bold('🐳')} Welcome to the JHipster Docker Compose Sub-Generator ${chalk.bold('🐳')}`)); this.log.log(chalk.white(`Files will be generated in folder: ${chalk.yellow(this.destinationRoot())}`)); }, checkDocker, async checkDockerCompose({ control }) { if (this.skipChecks) return; if (!control.environmentHasDockerCompose) { throw new Error(`Docker Compose V2 is not installed on your computer. Read https://docs.docker.com/compose/install/ `); } }, }); } get [BaseWorkspacesGenerator.INITIALIZING]() { return this.delegateTasksToBlueprint(() => this.initializing); } get prompting() { return this.asPromptingTaskGroup({ askForDirectoryPath, }); } get [BaseWorkspacesGenerator.PROMPTING]() { return this.delegateTasksToBlueprint(() => this.prompting); } get preparing() { return this.asPreparingTaskGroup({ setWorkspacesRoot() { this.setWorkspacesRoot(this.destinationPath(this.jhipsterConfig.directoryPath)); }, }); } get [BaseWorkspacesGenerator.PREPARING]() { return this.delegateTasksToBlueprint(() => this.preparing); } get promptingWorkspaces() { return this.asPromptingWorkspacesTaskGroup({ askForMonitoring, askForClustersModeWorkspace, askForServiceDiscoveryWorkspace, askForApisixGateway, }); } get [BaseWorkspacesGenerator.PROMPTING_WORKSPACES]() { return this.delegateTasksToBlueprint(() => this.promptingWorkspaces); } get configuringWorkspaces() { return this.asConfiguringWorkspacesTaskGroup({ configureBaseDeployment({ applications }) { this.jhipsterConfig.jwtSecretKey ??= createBase64Secret(this.options.reproducibleTests); if (applications.some(app => app.serviceDiscoveryTypeEureka)) { this.jhipsterConfig.adminPassword = this.jhipsterConfig.adminPassword ?? 'admin'; } }, }); } get [BaseWorkspacesGenerator.CONFIGURING_WORKSPACES]() { return this.delegateTasksToBlueprint(() => this.configuringWorkspaces); } get loadingWorkspaces() { return this.asLoadingWorkspacesTaskGroup({ loadBaseDeployment({ deployment, applications }) { deployment.jwtSecretKey = this.jhipsterConfig.jwtSecretKey; loadDockerDependenciesTask.call(this, { context: deployment }); loadDockerElasticsearchVersion.call(this, { springBoot4: applications.some(app => app.springBoot4), dockerContainers: deployment.dockerContainers, }); }, }); } get [BaseWorkspacesGenerator.LOADING_WORKSPACES]() { return this.delegateTasksToBlueprint(() => this.loadingWorkspaces); } get preparingWorkspaces() { return this.asPreparingWorkspacesTaskGroup({ prepareDeployment({ deployment, applications }) { this.prepareDeploymentDerivedProperties({ deployment, applications }); }, }); } get [BaseWorkspacesGenerator.PREPARING_WORKSPACES]() { return this.delegateTasksToBlueprint(() => this.preparingWorkspaces); } get default() { return this.asDefaultTaskGroup({ async setAppsYaml({ deployment, applications }) { const faker = await createFaker(); deployment.keycloakRedirectUris = ''; deployment.appsYaml = applications.map(appConfig => { const lowercaseBaseName = appConfig.baseName.toLowerCase(); const parentConfiguration = {}; const path = this.workspacePath(appConfig.appFolder); // Add application configuration const yaml = parseYaml(this.fs.read(`${path}/src/main/docker/app.yml`)); const yamlConfig = yaml.services.app; if (yamlConfig.depends_on) { yamlConfig.depends_on = Object.fromEntries(Object.entries(yamlConfig.depends_on).map(([serviceName, config]) => { if (['keycloak', 'jhipster-registry', 'consul'].includes(serviceName)) { return [serviceName, config]; } return [`${lowercaseBaseName}-${serviceName}`, config]; })); } if (appConfig.applicationTypeGateway || appConfig.applicationTypeMonolith) { if (deployment.keycloakSecrets === undefined && appConfig.authenticationTypeOauth2) { faker.seed(stringHashCode(appConfig.baseName)); deployment.keycloakSecrets = Array.from(Array(6), () => faker.string.uuid()); } deployment.keycloakRedirectUris += `"http://localhost:${appConfig.composePort}/*", "https://localhost:${appConfig.composePort}/*", `; if (appConfig.devServerPort !== undefined) { deployment.keycloakRedirectUris += `"http://localhost:${appConfig.devServerPort}/*", `; } if (appConfig.devServerPortProxy !== undefined) { deployment.keycloakRedirectUris += `"http://localhost:${appConfig.devServerPortProxy}/*", `; } // Split ports by ":" and take last 2 elements to skip the hostname/IP if present const ports = yamlConfig.ports[0].split(':').slice(-2); ports[0] = appConfig.composePort; yamlConfig.ports[0] = ports.join(':'); } if (yamlConfig.environment) { yamlConfig.environment = yamlConfig.environment.map((envOption) => { // Doesn't applies to keycloak, jhipster-registry and consul. // docker-compose changes the container name to `${lowercaseBaseName}-${databaseType}`. // we need to update the environment urls to the new container host. [ 'SPRING_R2DBC_URL', 'SPRING_DATASOURCE_URL', 'SPRING_LIQUIBASE_URL', 'SPRING_NEO4J_URI', 'SPRING_MONGODB_URI', 'JHIPSTER_CACHE_REDIS_SERVER', 'SPRING_ELASTICSEARCH_URIS', ].forEach(varName => { if (envOption.startsWith(varName)) { envOption = envOption .replace('://', `://${lowercaseBaseName}-`) .replace('oracle:thin:@', `oracle:thin:@${lowercaseBaseName}-`); } }); ['JHIPSTER_CACHE_MEMCACHED_SERVERS', 'SPRING_COUCHBASE_CONNECTION_STRING', 'SPRING_CASSANDRA_CONTACTPOINTS'].forEach(varName => { if (envOption.startsWith(varName)) { envOption = envOption.replace(`${varName}=`, `${varName}=${lowercaseBaseName}-`); } }); return envOption; }); } if (appConfig.applicationTypeMonolith && deployment.monitoring === PROMETHEUS) { yamlConfig.environment.push('JHIPSTER_LOGGING_LOGSTASH_ENABLED=false', 'MANAGEMENT_METRICS_EXPORT_PROMETHEUS_ENABLED=true'); } if (deployment.serviceDiscoveryType === EUREKA) { // Set the JHipster Registry password yamlConfig.environment.push(`JHIPSTER_REGISTRY_PASSWORD=${deployment.adminPassword}`); } const hasNoServiceDiscovery = !deployment.serviceDiscoveryType && deployment.serviceDiscoveryType !== NO_SERVICE_DISCOVERY; if (hasNoServiceDiscovery && appConfig.skipClient) { yamlConfig.environment.push('SERVER_PORT=80'); // to simplify service resolution in docker/k8s } parentConfiguration[lowercaseBaseName] = yamlConfig; // Add database configuration if (appConfig.databaseTypeAny && !appConfig.prodDatabaseTypeOracle) { const database = appConfig.databaseTypeSql ? appConfig.prodDatabaseType : appConfig.databaseType; const relativePath = normalize(pathjs.relative(this.destinationRoot(), `${path}/src/main/docker`)); const databaseYaml = parseYaml(this.fs.read(`${path}/src/main/docker/${database}.yml`)); const databaseServiceName = `${lowercaseBaseName}-${database}`; let databaseYamlConfig = databaseYaml.services[database]; // Don't export database ports delete databaseYamlConfig.ports; if (appConfig.databaseTypeCassandra) { // migration service config const cassandraMigrationYaml = parseYaml(this.fs.read(`${path}/src/main/docker/cassandra-migration.yml`)); const cassandraMigrationConfig = cassandraMigrationYaml.services[`${database}-migration`]; cassandraMigrationConfig.build.context = relativePath; const cqlFilesRelativePath = normalize(pathjs.relative(this.destinationRoot(), `${path}/src/main/resources/config/cql`)); cassandraMigrationConfig.volumes[0] = `${cqlFilesRelativePath}:/cql:ro`; parentConfiguration[`${databaseServiceName}-migration`] = cassandraMigrationConfig; } if (appConfig.databaseTypeCouchbase) { databaseYamlConfig.build.context = relativePath; } if (deployment.clusteredDbApps?.includes(appConfig.appFolder)) { const clusterDbYaml = parseYaml(this.fs.read(`${path}/src/main/docker/${database}-cluster.yml`)); const dbNodeConfig = clusterDbYaml.services[`${database}-node`]; dbNodeConfig.build.context = relativePath; databaseYamlConfig = clusterDbYaml.services[database]; delete databaseYamlConfig.ports; if (appConfig.databaseTypeCouchbase) { databaseYamlConfig.build.context = relativePath; } parentConfiguration[`${databaseServiceName}-node`] = dbNodeConfig; if (appConfig.databaseTypeMongodb) { parentConfiguration[`${databaseServiceName}-config`] = clusterDbYaml.services[`${database}-config`]; } } parentConfiguration[databaseServiceName] = databaseYamlConfig; } if (appConfig.searchEngineElasticsearch) { // Add search engine configuration const searchEngine = appConfig.searchEngine; const searchEngineYaml = parseYaml(this.fs.read(`${path}/src/main/docker/${searchEngine}.yml`)); const searchEngineConfig = searchEngineYaml.services[searchEngine]; delete searchEngineConfig.ports; parentConfiguration[`${lowercaseBaseName}-${searchEngine}`] = searchEngineConfig; } // Add Memcached support if (appConfig.cacheProviderMemcached) { const memcachedYaml = parseYaml(this.readDestination(`${path}/src/main/docker/memcached.yml`).toString()); const memcachedConfig = memcachedYaml.services.memcached; delete memcachedConfig.ports; parentConfiguration[`${lowercaseBaseName}-memcached`] = memcachedConfig; } // Add Redis support if (appConfig.cacheProviderRedis) { const redisYaml = parseYaml(this.readDestination(`${path}/src/main/docker/redis.yml`).toString()); const redisConfig = redisYaml.services.redis; delete redisConfig.ports; parentConfiguration[`${lowercaseBaseName}-redis`] = redisConfig; } // Expose authenticationType deployment.authenticationType = appConfig.authenticationType; // Dump the file let yamlString = stringifyYaml(parentConfiguration, { indent: 2, lineWidth: 0 }); // Add extra indentation for each lines const yamlArray = yamlString.split('\n'); for (let j = 0; j < yamlArray.length; j++) { yamlArray[j] = ` ${yamlArray[j]}`; } yamlString = yamlArray.join('\n'); return yamlString; }); }, }); } get [BaseWorkspacesGenerator.DEFAULT]() { return this.delegateTasksToBlueprint(() => this.default); } get writing() { return this.asWritingTaskGroup({ cleanupOldFilesTask, writeFiles, }); } get [BaseWorkspacesGenerator.WRITING]() { return this.delegateTasksToBlueprint(() => this.writing); } get end() { return this.asEndTaskGroup({ end({ applications }) { this.checkApplicationsDockerImages({ applications }); this.log.verboseInfo(`You can launch all your infrastructure by running : ${chalk.cyan('docker compose up -d')}`); const uiApplications = applications.filter(app => (app.applicationTypeGateway || app.applicationTypeMonolith) && app.clientFrameworkAny); if (uiApplications.length > 0) { this.log.log('\nYour applications will be accessible on these URLs:'); for (const application of uiApplications) { this.log.verboseInfo(`\t- ${application.baseName}: http://localhost:${application.composePort}`); } this.log.log('\n'); } }, }); } get [BaseWorkspacesGenerator.END]() { return this.delegateTasksToBlueprint(() => this.end); } checkApplicationsDockerImages({ applications }) { this.log.log('\nChecking Docker images in applications directories...'); let imagePath = ''; let runCommand = ''; let hasWarning = false; let warningMessage = 'To generate the missing Docker image(s), please run:\n'; applications.forEach(application => { if (application.buildToolGradle) { imagePath = this.workspacePath(application.appFolder, 'build/jib-cache'); runCommand = `./gradlew bootJar -Pprod jibDockerBuild${process.arch === 'arm64' ? ' -PjibArchitecture=arm64' : ''}`; } else if (application.buildToolMaven) { imagePath = this.workspacePath(application.appFolder, '/target/jib-cache'); runCommand = `./mvnw -ntp -Pprod verify jib:dockerBuild${process.arch === 'arm64' ? ' -Djib-maven-plugin.architecture=arm64' : ''}`; } if (!existsSync(imagePath)) { hasWarning = true; warningMessage += ` ${chalk.cyan(runCommand)} in ${this.workspacePath(application.appFolder)}\n`; } }); if (hasWarning) { this.log.warn('Docker Compose configuration generated, but no Jib cache found'); this.log.warn('If you forgot to generate the Docker image for this application, please run:'); this.log.log(chalk.red(warningMessage)); } else { this.log.verboseInfo(`${chalk.bold.green('Docker Compose configuration successfully generated!')}`); } } prepareDeploymentDerivedProperties({ deployment, applications }) { deployment.usesOauth2 = applications.some(appConfig => appConfig.authenticationTypeOauth2); deployment.useKafka = applications.some(appConfig => appConfig.messageBrokerKafka); deployment.usePulsar = applications.some(appConfig => appConfig.messageBrokerPulsar); deployment.useMemcached = applications.some(appConfig => appConfig.cacheProviderMemcached); deployment.useRedis = applications.some(appConfig => appConfig.cacheProviderRedis); deployment.includesApplicationTypeGateway = applications.some(appConfig => appConfig.applicationTypeGateway); deployment.entryPort = 8080; deployment.appConfigs = applications; } }