diff --git a/Jenkinsfile b/Jenkinsfile index dcc1192..50e8b69 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,6 +3,14 @@ pipeline { agent any stages { + stage("Initialization") { + steps { + script { + def version = sh(returnStdout: true, script: 'grep \'version=\' gradle.properties | cut -d\'=\' -f2') + buildName "${env.GIT_BRANCH.replace("origin/", "")}@${version}" + } + } + } stage('Build') { steps { checkout scm diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ee76e8 --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# Springboard Plugin + +A Gradle plugin for managing and configuring Edifice applications built on the ENT Core framework. + +## Overview + +The Springboard Plugin simplifies the development workflow for Edifice applications by providing automated tasks for: +- Configuration file generation +- Deployment extraction and organization +- Translation and help documentation management +- Theme extraction +- Integration testing with Gatling +- Module version management from GitHub + +## Requirements + +- Java 8 +- Gradle 4.5 +- Docker and Docker Compose (for build and deployment) + +## Installation + +### Using the Build Script + +The plugin includes a build script that uses Docker Compose for consistent builds: + +```bash +# Clean the project +./build.sh clean + +# Install the plugin locally +./build.sh install + +# Publish to repository (requires credentials) +./build.sh publish +``` + +### Manual Installation + +```bash +gradle install +``` + +## Usage + +### Applying the Plugin + +In your project's `build.gradle`: + +```groovy +plugins { + id 'fr.wseduc.springboard' version '3.2-zookeeper-SNAPSHOT' +} +``` + +### Available Tasks + +#### `init` +Initialize a new Edifice project with all necessary files and directories: +- Creates directory structure (mods, deployments, i18n, etc.) +- Extracts deployments, helps, and translations +- Generates sample data and configuration files +- Sets up Docker Compose configuration +- Creates integration test scaffolding + +```bash +gradle init +``` + +#### `generateConf` +Generate the `ent-core.json` configuration file from templates: + +```bash +gradle generateConf +``` + +This task: +- Reads `conf.properties` and `gradle.properties` +- Merges with `default.properties` +- Automatically fetches missing base modules versions from GitHub +- Generates `ent-core.json` from `ent-core.json.template` + +#### `extractDeployments` +Extract deployment artifacts from dependencies: + +```bash +gradle extractDeployments +``` + +#### `extractHelps` +Extract help documentation from dependencies: + +```bash +gradle extractHelps +``` + +#### `extractTranslations` +Extract i18n translation files: + +```bash +gradle extractTranslations +``` + +#### `extractTheme` +Extract theme assets: + +```bash +gradle extractTheme +``` + +#### `integrationTest` +Run Gatling integration tests: + +```bash +gradle integrationTest +``` + +## Module Version Management + +The plugin can automatically fetch the latest versions of Edifice modules from GitHub if they're not specified in your `gradle.properties`. Supported modules include: + +- `mod-pdf-generator` +- `mod-mongo-persistor` +- `mod-image-resizer` +- `mod-zip` +- `mod-postgresql` +- `mod-json-schema-validator` +- `mod-sftp` +- `mod-webdav` +- `mod-sms-sender` + +### Environment Variables + +- `MODS_DEFAULT_BRANCH`: Set the default branch to fetch module versions from (defaults to trying `master`, then `main`) + +## Project Structure + +After running the `init` task, your project will have: + +``` +. +├── conf.properties # User configuration +├── default.properties # Default configuration values +├── ent-core.json.template # Configuration template +├── ent-core.json # Generated configuration +├── package.json # Node.js dependencies +├── docker-compose.yml # Docker setup +├── deployments/ # Extracted deployment artifacts +├── i18n/ # Translation files +├── static/help/ # Help documentation +├── mods/ # Application modules +├── sample-be1d/ # Sample data +├── neo4j-conf/ # Neo4j configuration +├── docker-entrypoint-initdb.d/ # Database initialization scripts +└── src/ + └── test/ + └── scala/ + └── org/entcore/test/ + ├── scenarios/ # Gatling test scenarios + └── simulations/ # Gatling simulations +``` + +## Configuration Files + +### conf.properties +User-specific configuration properties that override defaults. + +### default.properties +Default configuration values merged during configuration generation. + +### gradle.properties +Module versions and build configuration. + +## Docker Support + +The plugin includes Docker Compose configuration with platform-specific support: +- Automatically detects M1/ARM64 Macs +- Uses appropriate Docker images for the platform +- Configures PostgreSQL, MongoDB, and Neo4j services + +## Development + +### Building the Plugin + +```bash +./build.sh install +``` + +### Publishing + +```bash +# Set credentials in ~/.gradle/gradle.properties: +# odeUsername= +# odePassword= + +./build.sh publish +``` + +## License + +See LICENSE file for details. + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Submit a pull request + +## Support + +For issues and questions, please refer to the Edifice documentation or create an issue in the repository. diff --git a/build.sh b/build.sh index dffd62a..c795444 100755 --- a/build.sh +++ b/build.sh @@ -8,12 +8,12 @@ fi clean () { - docker-compose run --rm -u "$USER_UID:$GROUP_GID" gradle gradle clean + docker compose run --rm -u "$USER_UID:$GROUP_GID" gradle gradle clean } install() { -# docker-compose run --rm -u "$USER_UID:$GROUP_GID" gradle gradle install publishToMavenLocal - docker-compose run --rm -u "$USER_UID:$GROUP_GID" gradle gradle install +# docker compose run --rm -u "$USER_UID:$GROUP_GID" gradle gradle install publishToMavenLocal + docker compose run --rm -u "$USER_UID:$GROUP_GID" gradle gradle install } publish() { @@ -24,7 +24,7 @@ publish() { echo "sonatypeUsername=$NEXUS_SONATYPE_USERNAME" >> "?/.gradle/gradle.properties" echo "sonatypePassword=$NEXUS_SONATYPE_PASSWORD" >> "?/.gradle/gradle.properties" fi - docker-compose run --rm -u "$USER_UID:$GROUP_GID" gradle gradle publish + docker compose run --rm -u "$USER_UID:$GROUP_GID" gradle gradle publish } for param in "$@" diff --git a/gradle.properties b/gradle.properties index 2d77916..1078511 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ group=fr.wseduc archivesBaseName=springboard-plugin -version=4.0-SNAPSHOT +version=4.0-zookeeper-SNAPSHOT diff --git a/src/main/groovy/fr/wseduc/gradle/springboard/FileUtils.groovy b/src/main/groovy/fr/wseduc/gradle/springboard/FileUtils.groovy index b8b9ae2..daa7d36 100644 --- a/src/main/groovy/fr/wseduc/gradle/springboard/FileUtils.groovy +++ b/src/main/groovy/fr/wseduc/gradle/springboard/FileUtils.groovy @@ -4,11 +4,28 @@ import groovy.text.SimpleTemplateEngine import org.gradle.api.Project class FileUtils { + + /** + * Map of module property names to module names on GitHub + * so we can fetch their version if not specified in gradle.properties. + */ + static final Map MOD_NAME_MAPPINGS = [ + modPdfgenerator: 'mod-pdf-generator', + modMongoPersistorVersion: 'mod-mongo-persistor', + modImageResizerVersion: 'mod-image-resizer', + modZipVersion: 'mod-zip', + modPostgresVersion: 'mod-postgresql', + modJsonschemavalidatorVersion: 'mod-json-schema-validator', + modSftpVersion: 'OPEN-ENT-NG/mod-sftp', + modWebdavVersion: 'OPEN-ENT-NG/mod-webdav', + modSmsproxyVersion: 'mod-sms-sender' + ] static def createFile(String propertiesFile, String gradleFile, String templateFileName, - String outputFileName) { + String outputFileName, + logger) { def props = new Properties() def file = new File(propertiesFile) def rootDirectory = file.getParentFile() @@ -32,6 +49,7 @@ class FileUtils { bindings[prop] = defaultProps.getProperty(prop) } } + populateModuleVersionsNotInBindings(bindings, logger) def engine = new SimpleTemplateEngine() def templateFile = new File(templateFileName) def output = engine.createTemplate(templateFile).make(bindings) @@ -43,6 +61,55 @@ class FileUtils { fileWriter.close() } + /** + * For each module defined in MOD_NAME_MAPPINGS, if the corresponding + * property is not defined in the bindings map, fetch the latest release + * version from GitHub and add it to the bindings. + */ + static def populateModuleVersionsNotInBindings(Map bindings, logger) { + String modsDefaultBranch = System.getenv("MODS_DEFAULT_BRANCH") + MOD_NAME_MAPPINGS.each { key, modName -> + if (!bindings.containsKey(key)) { + logger.lifecycle("Fetching latest release version for module: " + modName) + // Fetch raw pom.xml from GitHub for the module. It will successively try + // the default branch (if defined in env MODS_DEFAULT_BRANCH), then master, + // then main. + // If it still does not exist raise an error + String[] branches = [] + if (modsDefaultBranch != null && !modsDefaultBranch.isEmpty()) { + branches = [modsDefaultBranch, 'master', 'main'] + } else { + branches = ['master', 'main'] + } + String latestVersion = null + for (String branch : branches) { + try { + String modUrl; + if (modName.contains('/')) { + modUrl = modName + } else { + modUrl = "edificeio/" + modName + } + URL pomUrl = new URL("https://raw.githubusercontent.com/" + modUrl + "/refs/heads/" + branch + "/pom.xml") + InputStream pomStream = pomUrl.openStream() + String pomContent = pomStream.getText("UTF-8") + // Parse the xml and extract the version of the artifact + def pomXml = new XmlSlurper().parseText(pomContent) + latestVersion = pomXml.version.text() + break + } catch (Exception e) { + // Continue to next branch + } + } + if (latestVersion == null) { + throw new RuntimeException("Could not find the latest version of module: " + modName) + } + logger.lifecycle("Using latest version for module " + modName + " : " + latestVersion) + bindings[key] = latestVersion + } + } + } + /** * Removes from the specified props the properties that are not * useful for the generation of the entcore.json diff --git a/src/main/groovy/fr/wseduc/gradle/springboard/SpringboardPlugin.groovy b/src/main/groovy/fr/wseduc/gradle/springboard/SpringboardPlugin.groovy index a357e6f..0f34443 100644 --- a/src/main/groovy/fr/wseduc/gradle/springboard/SpringboardPlugin.groovy +++ b/src/main/groovy/fr/wseduc/gradle/springboard/SpringboardPlugin.groovy @@ -11,7 +11,12 @@ class SpringboardPlugin implements Plugin { void apply(Project project) { project.task("generateConf") << { def rootDir = project.getRootDir().getAbsolutePath() - FileUtils.createFile("${rootDir}/conf.properties", "${rootDir}/gradle.properties", "${rootDir}/ent-core.json.template", "${rootDir}/ent-core.json") + FileUtils.createFile( + "${rootDir}/conf.properties", + "${rootDir}/gradle.properties", + "${rootDir}/ent-core.json.template", + "${rootDir}/ent-core.json", + project.logger) } project.task("extractDeployments") << { diff --git a/src/main/resources/conf.properties b/src/main/resources/conf.properties index c4f52f8..9c03e47 100644 --- a/src/main/resources/conf.properties +++ b/src/main/resources/conf.properties @@ -51,7 +51,6 @@ passwordRegex=.* skins={"localhost:8090":"raw"} activationAutoLogin=false signKey= -xitiSwitch= directoryCalendarSwitch= adminZimbra= samlMetadataFolder= @@ -76,4 +75,8 @@ cantooOptionalFeatureScriptPath= restrictCRUDToADMC= visiblesSearchType= getVisibleStrategy= -debounceTimeToAutoSave=5000 \ No newline at end of file +debounceTimeToAutoSave=5000 +xitiSwitch= +magnetoWebsocketMaxUsers=5000 +magnetoWebsocketMaxUsersPerBoard=500 +magnetoWebsocketPort=9092 diff --git a/src/main/resources/conf/vertx/logback.xml b/src/main/resources/conf/vertx/logback.xml new file mode 100755 index 0000000..7c8eefc --- /dev/null +++ b/src/main/resources/conf/vertx/logback.xml @@ -0,0 +1,33 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss} %level %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/conf/vertx/logging.properties b/src/main/resources/conf/vertx/logging.properties new file mode 100755 index 0000000..1886abf --- /dev/null +++ b/src/main/resources/conf/vertx/logging.properties @@ -0,0 +1,13 @@ +handlers=com.opendigitaleducation.launcher.logger.ENTLogHandler +com.opendigitaleducation.launcher.logger.ENTLogHandler.level=INFO + +.level=INFO +org.vertx.level=INFO +com.hazelcast.level=SEVERE +io.netty.util.internal.PlatformDependent.level=SEVERE + +ACCESS.level=FINEST +ACCESS.handlers=com.opendigitaleducation.launcher.logger.AccessLogHandler +ACCESS.useParentHandlers=false +ACCESS.com.opendigitaleducation.launcher.logger.AccessLogHandler.level=FINEST +ACCESS.com.opendigitaleducation.launcher.logger.AccessLogHandler.formatter=com.opendigitaleducation.launcher.logger.JsonAccessFormatter diff --git a/src/main/resources/docker-compose.mac.yml b/src/main/resources/docker-compose.mac.yml index f63af21..cd037be 100644 --- a/src/main/resources/docker-compose.mac.yml +++ b/src/main/resources/docker-compose.mac.yml @@ -39,10 +39,10 @@ services: - SERVICES=s3 - AWS_DEFAULT_REGION=us-east-1 volumes: - - ./cluster-config/init-s3:/etc/localstack/init + - ./conf/cluster-config/init-s3:/etc/localstack/init vertx: - image: opendigitaleducation/vertx-service-launcher:3.0-SNAPSHOT + image: opendigitaleducation/vertx-service-launcher:4.0-zookeeper-SNAPSHOT user: "$DEFAULT_DOCKER_USER" ports: - "5000:5000" @@ -50,6 +50,8 @@ services: - ./assets:/srv/springboard/assets - ./mods:/srv/springboard/mods - ./ent-core.json:/srv/springboard/conf/vertx.conf + - ./conf/vertx/logback.xml:/srv/springboard/conf/logback.xml + - ./conf/vertx/logging.properties:/srv/springboard/conf/logging.properties - ./conf/cluster-config/zookeeper.json:/srv/springboard/conf/zookeeper.json - ./aaf-duplicates-test:/home/wse/aaf - ~/.m2:/home/vertx/.m2 @@ -65,7 +67,7 @@ services: - zookeeper environment: - MODE=cluster - - ENABLED_SERVICES=o + - ENABLED_SERVICES= - DEBUG_SUSPEND=n - ENABLE_REMOTE_DEBUG=true pdf: diff --git a/src/main/resources/docker-compose.yml b/src/main/resources/docker-compose.yml index 81de1c0..7e22867 100644 --- a/src/main/resources/docker-compose.yml +++ b/src/main/resources/docker-compose.yml @@ -43,10 +43,10 @@ services: - SERVICES=s3 - AWS_DEFAULT_REGION=us-east-1 volumes: - - ./cluster-config/init-s3:/etc/localstack/init + - ./conf/cluster-config/init-s3:/etc/localstack/init vertx: - image: opendigitaleducation/vertx-service-launcher:3.0-SNAPSHOT + image: opendigitaleducation/vertx-service-launcher:4.0-zookeeper-SNAPSHOT user: "$DEFAULT_DOCKER_USER" ports: #REMOVE_BY_CI - "5000:5000" #REMOVE_BY_CI @@ -54,6 +54,8 @@ services: - ./assets:/srv/springboard/assets - ./mods:/srv/springboard/mods - ./ent-core.json:/srv/springboard/conf/vertx.conf + - ./conf/vertx/logback.xml:/srv/springboard/conf/logback.xml + - ./conf/vertx/logging.properties:/srv/springboard/conf/logging.properties - ./conf/cluster-config/zookeeper.json:/srv/springboard/conf/zookeeper.json - ./aaf-duplicates-test:/home/wse/aaf - ~/.m2:/home/vertx/.m2 @@ -87,6 +89,10 @@ services: image: neo4j:3.1 volumes: - ./neo4j-conf:/conf + ulimits: + nofile: + soft: 65536 + hard: 65536 redis: image: redis:7.4.0-alpine @@ -109,8 +115,8 @@ services: cap_add: - IPC_LOCK ports: #REMOVE_BY_CI - - "9200:9200" - - "9300:9300" + - "9200:9200" #REMOVE_BY_CI + - "9300:9300" #REMOVE_BY_CI postgres: image: postgres:14.13 diff --git a/src/main/resources/ent-core.json.template b/src/main/resources/ent-core.json.template index 4b814ba..407d926 100644 --- a/src/main/resources/ent-core.json.template +++ b/src/main/resources/ent-core.json.template @@ -13,7 +13,7 @@ }, "services" : [ { - "name": "io.vertx~mod-mongo-persistor~4.0-SNAPSHOT", + "name": "io.vertx~mod-mongo-persistor~${modMongoPersistorVersion}", "waitDeploy" : true, "worker": true, "multi-threaded": true, @@ -30,7 +30,7 @@ } }, { - "name" : "fr.wseduc~mod-image-resizer~3.0-SNAPSHOT", + "name" : "fr.wseduc~mod-image-resizer~${modImageResizerVersion}", "worker": true, "multi-threaded": true, "waitDeploy" : true, @@ -58,7 +58,7 @@ } }, { - "name": "fr.wseduc~mod-zip~3.0-SNAPSHOT", + "name": "fr.wseduc~mod-zip~${modZipVersion}", "waitDeploy" : true, "worker": true, "config":{ @@ -66,7 +66,7 @@ } }, { - "name": "fr.wseduc~mod-postgresql~2.0-SNAPSHOT", + "name": "fr.wseduc~mod-postgresql~${modPostgresVersion}", "waitDeploy" : true, "worker": true, "multi-threaded" : true, @@ -79,7 +79,7 @@ }, <% if (pgAdminUser != null && !pgAdminUser.trim().isEmpty()) { %> { - "name": "fr.wseduc~mod-postgresql~2.0-SNAPSHOT", + "name": "fr.wseduc~mod-postgresql~${modPostgresVersion}", "waitDeploy" : true, "worker": true, "multi-threaded" : true, @@ -92,7 +92,7 @@ }, <% } %> { - "name": "com.opendigitaleducation~mod-json-schema-validator~2.0-SNAPSHOT", + "name": "com.opendigitaleducation~mod-json-schema-validator~${modJsonschemavalidatorVersion}", "waitDeploy" : true, "worker": true, "multi-threaded" : true, @@ -101,7 +101,7 @@ }, <% if (modSftp != null && !modSftp.trim().isEmpty()) { %> { - "name": "fr.cgi~mod-sftp~2.0-SNAPSHOT", + "name": "fr.cgi~mod-sftp~${modSftpVersion}", "waitDeploy": true, "config": { "main" : "fr.cgi.sftp.Sftp", @@ -112,7 +112,7 @@ <% } %> <% if (webdavHost != null && !webdavHost.trim().isEmpty()) { %> { - "name": "fr.wseduc~mod-webdav~2.0-SNAPSHOT", + "name": "fr.wseduc~mod-webdav~${modWebdavVersion}", "config": { "address" : "webdav" <% if ("true".equals(webdavCredential)) { %> @@ -129,7 +129,7 @@ }, <% } %> { - "name": "fr.wseduc~mod-sms-proxy~2.0-SNAPSHOT", + "name": "fr.wseduc~mod-sms-proxy~${modSmsproxyVersion}", "waitDeploy" : true, "worker": true, "config":{ @@ -138,7 +138,7 @@ } }, { - "name": "fr.wseduc~mod-pdf-generator~2.0-SNAPSHOT", + "name": "fr.wseduc~mod-pdf-generator~${modPdfgenerator}", "waitDeploy" : true, "worker": true, "config":{ @@ -264,18 +264,8 @@ <% if ("true".equals(bprEnabled)) { %> "library-enabled": true, "library-api-url": "${bprUrl}", - "library-token": "${bprToken}", + "library-token": "${bprToken}" <% } %> - "publicConf": { - <% if ("true".equals(xitiSwitch)) { %> - "xiti": { - "ID_SERVICE": { - "default": "", - "/admin-console": "" - } - } - <% } %> - } } }, { @@ -365,15 +355,6 @@ "restrictCRUDToADMC": false <% } %> } - <% if ("true".equals(xitiSwitch)) { %> - ,"xiti": { - "ID_SERVICE": { - "default": 10, - "/userbook/mon-compte": "", - "/admin-console": "" - } - } - <% } %> } } }, @@ -394,16 +375,7 @@ "app-type": "END_USER", "port": 8011, "wsPort" : 6502, - "mongo-address" : "wse.mongodb.persistor", - "publicConf": { - <% if ("true".equals(xitiSwitch)) { %> - "xiti": { - "ID_SERVICE": { - "default": 1 - } - } - <% } %> - } + "mongo-address" : "wse.mongodb.persistor" } }, { @@ -440,17 +412,7 @@ } } <% } %>, - "visibles-search-type": "${visiblesSearchType}", - "publicConf": { - <% if ("true".equals(xitiSwitch)) { %> - "xiti": { - "ID_SERVICE": { - "default": "", - "/admin-console": "" - } - } - <% } %> - } + "visibles-search-type": "${visiblesSearchType}" } }, { @@ -488,9 +450,6 @@ {"name": "welcomeMessage", "url": "/auth/admin-welcome-message", "allowed": ["SUPER_ADMIN"]}, {"name": "flashMessage", "url": "/timeline/flashmsg/admin", "allowed": ["SUPER_ADMIN"]}, {"name": "embed", "url": "/infra/embed/admin", "allowed": ["SUPER_ADMIN"]} - <% if ("true".equals(xitiSwitch)) { %> - ,{"name": "xiti", "url": "/xiti/admin-console", "allowed": ["SUPER_ADMIN"]} - <% } %> <% if ("true".equals(keyring)) { %> ,{"name": "keyring", "url": "/sso/keyring/admin-console", "allowed": ["SUPER_ADMIN"]} <% } %> @@ -546,13 +505,6 @@ "get-visible-strategy": "${getVisibleStrategy}", "publicConf": { "debounce-time-to-auto-save": ${debounceTimeToAutoSave} - <% if ("true".equals(xitiSwitch)) { %> - ,"xiti": { - "ID_SERVICE": { - "default": 10 - } - } - <% } %> } } },