diff --git a/.eslintrc.js b/.eslintrc.js index a5496d9..137ffb6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -32,8 +32,8 @@ module.exports = { ignorePatterns: [ 'docs/*', 'dist/*', + 'src/lidy/*', 'src/assets/index.js', - 'src/antlr/*', ], // add your custom rules here diff --git a/changelog.md b/changelog.md index eb6508d..14aed94 100644 --- a/changelog.md +++ b/changelog.md @@ -5,8 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) -## [Unreleased] +## [0.1.0] ### Added -- Setup project. +- Add yaml syntax highlighting +- Support for parsing Docker-Compose files. +- Add Docker-Compose metadata. +- leto-modelizer-plugin-core version 0.21.0 \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index a8bc1e2..35b13d7 100755 --- a/jest.config.js +++ b/jest.config.js @@ -1,21 +1,23 @@ -/* - * For a detailed explanation regarding each configuration property, visit: - * https://jestjs.io/docs/configuration - */ - module.exports = { globals: { __DEV__: true, }, // Jest assumes we are testing in node environment, specify jsdom environment instead testEnvironment: 'node', - // Needed in JS codebases too because of feature flags - coveragePathIgnorePatterns: ['/node_modules/', '.d.ts$'], + testEnvironmentOptions: { + customExportConditions: ['node', 'node-addons'], + }, + // noStackTrace: true, + // bail: true, + // cache: false, + // verbose: true, + // watch: true, testMatch: [ '/tests/unit/**/*.spec.js', ], - moduleFileExtensions: ['js'], + moduleFileExtensions: ['js', 'json'], moduleNameMapper: { + '^~/(.*)$': '/$1', '^src/(.*)$': '/src/$1', '^tests/(.*)$': '/tests/$1', 'package.json': 'package.json', @@ -24,9 +26,19 @@ module.exports = { 'node_modules', ], transform: { - '^.+\\.(js|jsx)?$': 'babel-jest', + '^.+\\.js?$': 'babel-jest', + }, + // Needed in JS codebases too because of feature flags + coveragePathIgnorePatterns: ['/node_modules/', '.d.ts$'], + coverageThreshold: { + global: { + // branches: 50, + // functions: 50, + // lines: 50, + // statements: 50 + }, }, - transformIgnorePatterns: ['/node_modules/(?!antlr)'], + transformIgnorePatterns: ['/node_modules/(?!lidy-js)'], testResultsProcessor: 'jest-sonar-reporter', collectCoverage: true, collectCoverageFrom: ['src/**/*.js'], diff --git a/jsdoc.config.json b/jsdoc.config.json index 7b018e7..433a69a 100644 --- a/jsdoc.config.json +++ b/jsdoc.config.json @@ -24,7 +24,7 @@ "navLinks": [ { "label": "Github", - "href": "https://github.com/ditrit/terrator-plugin#README.md" + "href": "https://github.com/ditrit/dockercomposator-plugin#README.md" } ] } diff --git a/package.json b/package.json index 6c837a2..22ef5c9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "lint:report": "eslint --max-warnings=0 --ext .js src -f json-relative > eslint.json", "prepare:docs": "sed -i 's#taffydb#@jsdoc/salty#g' node_modules/better-docs/publish.js", "test": "jest", - "test:coverage": "jest --coverage" + "test:coverage": "jest --coverage", + "generate:parser": "node scripts/generate_parser.js" }, "repository": { "type": "git", @@ -30,26 +31,31 @@ }, "homepage": "https://github.com/ditrit/dockercomposator-plugin#readme", "dependencies": { - "leto-modelizer-plugin-core": "github:ditrit/leto-modelizer-plugin-core#0.17.0" + "js-yaml": "^4.1.0", + "leto-modelizer-plugin-core": "github:ditrit/leto-modelizer-plugin-core#0.21.0", + "lidy-js": "github:ditrit/lidy-js.git#main" }, "devDependencies": { - "@babel/core": "=7.22.9", - "@babel/preset-env": "=7.22.9", - "babel-jest": "=29.6.1", - "babel-loader": "=9.1.3", - "better-docs": "=2.7.2", - "eslint": "=8.45.0", - "eslint-config-airbnb-base": "=15.0.0", - "eslint-formatter-json-relative": "=0.1.0", - "eslint-plugin-import": "=2.27.5", - "eslint-plugin-jest": "=27.2.3", - "eslint-plugin-jsdoc": "=46.4.4", - "eslint-webpack-plugin": "=4.0.1", - "jest": "=29.6.1", - "jest-environment-jsdom": "=29.6.1", - "jest-sonar-reporter": "=2.0.0", - "jsdoc": "=4.0.2", - "webpack": "=5.88.2", - "webpack-cli": "=5.1.4" + "@babel/core": "^7.23.2", + "@babel/preset-env": "^7.23.2", + "babel-jest": "^29.7.0", + "babel-loader": "^9.1.3", + "better-docs": "^2.7.2", + "eslint": "^8.51.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-formatter-json-relative": "^0.1.0", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jest": "^27.4.2", + "eslint-plugin-jsdoc": "^44.2.7", + "eslint-webpack-plugin": "^4.0.1", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-sonar-reporter": "^2.0.0", + "js-yaml": "^4.1.0", + "jsdoc": "^4.0.2", + "leto-modelizer-plugin-core": "github:ditrit/leto-modelizer-plugin-core#0.21.0", + "lidy-js": "github:ditrit/lidy-js#main", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4" } } diff --git a/public/icons/DefaultIcon.svg b/public/icons/DefaultIcon.svg new file mode 100644 index 0000000..25abf5a --- /dev/null +++ b/public/icons/DefaultIcon.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/public/icons/compose.svg b/public/icons/compose.svg new file mode 100644 index 0000000..738439a --- /dev/null +++ b/public/icons/compose.svg @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/public/icons/config.svg b/public/icons/config.svg new file mode 100644 index 0000000..833fc08 --- /dev/null +++ b/public/icons/config.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/public/icons/dockercomposator-plugin.svg b/public/icons/dockercomposator-plugin.svg index 8ec4448..31aa4f5 100644 --- a/public/icons/dockercomposator-plugin.svg +++ b/public/icons/dockercomposator-plugin.svg @@ -1 +1,13 @@ - \ No newline at end of file + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/icons/network.svg b/public/icons/network.svg new file mode 100644 index 0000000..3d31c44 --- /dev/null +++ b/public/icons/network.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/icons/secret.svg b/public/icons/secret.svg new file mode 100644 index 0000000..2a85bf0 --- /dev/null +++ b/public/icons/secret.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/service.svg b/public/icons/service.svg new file mode 100644 index 0000000..62744ce --- /dev/null +++ b/public/icons/service.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/volume.svg b/public/icons/volume.svg new file mode 100644 index 0000000..18163a0 --- /dev/null +++ b/public/icons/volume.svg @@ -0,0 +1,19 @@ + + + + + folder_plus [#1780] + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/models/DefaultContainer.svg b/public/models/DefaultContainer.svg index 4fc1f90..ae5d97b 100644 --- a/public/models/DefaultContainer.svg +++ b/public/models/DefaultContainer.svg @@ -18,8 +18,8 @@ @@ -29,15 +29,16 @@ + font-family="Roboto"> {{ id }} {{ definition.type }} + x="50%" y="72">{{ definition.displayName or definition.type }} + fill="white" + style="outline: 1px dashed #5B81A5; outline-offset: -2px" /> + ry="4" rx="4" /> @@ -19,9 +19,9 @@ + x="10" y="5" + fill="#F2C037" + viewBox="0 0 512 512"> @@ -32,16 +32,17 @@ {{ id }} {{ definition.type }} + x="50%" y="72">{{ definition.displayName or definition.type }} diff --git a/scripts/generate_parser.js b/scripts/generate_parser.js new file mode 100644 index 0000000..1e839cc --- /dev/null +++ b/scripts/generate_parser.js @@ -0,0 +1,3 @@ +import('lidy-js/parser/node_parse.js').then(({ preprocess }) => { + preprocess('src/lidy/dockerComposeGrammar.yml'); +}); diff --git a/src/assets/metadata/docker-compose.json b/src/assets/metadata/docker-compose.json new file mode 100644 index 0000000..d7f523d --- /dev/null +++ b/src/assets/metadata/docker-compose.json @@ -0,0 +1,365 @@ +[ + { + "type": "Docker-Compose", + "description": "Represents the Compose file, it's the parent of all other components and it contains the top-level version property", + "url": "https://docs.docker.com/compose/compose-file/03-compose-file", + "model": "DefaultContainer", + "displayName": "Docker Compose", + "icon": "compose", + "isContainer": true, + "attributes": [ + { + "name": "version", + "type": "String", + "required": true + } + ] + }, + { + "type": "Service", + "description": "A service is an abstract definition of a computing resource within an application which can be scaled or replaced independently from other components. Services are backed by a set of containers", + "url": "https://docs.docker.com/compose/compose-file/05-services/", + "model": "DefaultModel", + "icon": "service", + "displayName": "Service", + "isContainer": false, + "attributes": [ + { + "name": "image", + "type": "String", + "required": true + }, + { + "name": "build", + "type": "Object", + "expanded": true, + "attributes": [ + { + "name": "context", + "type": "String" + }, + { + "name": "dockerfile", + "type": "String" + }, + { + "name": "args", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + } + ] + }, + { + "name": "depends_on", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "Object", + "attributes": [ + { + "name": "service", + "type": "Link", + "linkRef": "Service", + "linkColor": "red" + }, + { + "name": "condition", + "type": "String" + } + ] + } + ] + }, + { + "name": "environment", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "ports", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "healthcheck", + "type": "Object", + "expanded": true, + "attributes": [ + { + "name": "test", + "type": "String" + }, + { + "name": "interval", + "type": "String" + }, + { + "name": "timeout", + "type": "String" + }, + { + "name": "retries", + "type": "Number" + } + ] + }, + { + "name": "networks", + "type": "Link", + "linkRef": "Network", + "linkColor": "purple" + }, + { + "name": "volumes", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "Object", + "attributes": [ + { + "name": "volume-name", + "type": "Link", + "linkRef": "Volume", + "linkColor": "green" + }, + { + "name": "mount-path", + "type": "String" + } + ] + } + ] + }, + { + "name": "configs", + "type": "Link", + "linkRef": "Config", + "linkColor": "blue" + }, + { + "name": "secrets", + "type": "Link", + "linkRef": "Secret", + "linkColor": "pink" + }, + { + "name": "command", + "type": "String" + }, + { + "name": "stdin_open", + "type": "Boolean" + }, + { + "name": "privileged", + "type": "Boolean" + }, + { + "name": "tty", + "type": "Boolean" + } + ] + }, + { + "type": "Volume", + "description": "Volumes are persistent data stores implemented by the container engine.", + "url": "https://docs.docker.com/compose/compose-file/07-volumes/", + "model": "DefaultModel", + "icon": "volume", + "displayName": "Volume", + "isContainer": false, + "attributes": [ + { + "name": "driver", + "type": "String", + "required": true + }, + { + "name": "driver_opts", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "labels", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "external", + "type": "Boolean" + } + ] + }, + { + "type": "Network", + "description": "Networks are the layer that allow services to communicate with each other.", + "url": "https://docs.docker.com/compose/compose-file/06-networks/", + "model": "DefaultModel", + "icon": "network", + "displayName": "Network", + "isContainer": false, + "attributes": [ + { + "name": "driver", + "type": "String", + "required": true + }, + { + "name": "driver_opts", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "enable_ipv6", + "type": "Boolean" + }, + { + "name": "ipam", + "type": "Object", + "expanded": true, + "attributes": [ + { + "name": "driver", + "type": "String" + }, + { + "name": "config", + "type": "Object", + "attributes": [ + { + "name": "subnet", + "type": "String" + }, + { + "name": "ip_range", + "type": "String" + }, + { + "name": "gateway", + "type": "Array" + }, + { + "name": "aux_adresses", + "type": "Array", + "attributes": [ + { + "name": "host", + "type": "String" + } + ] + } + ] + }, + { + "name": "options", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + } + ] + }, + { + "name": "labels", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "external", + "type": "Boolean" + } + ] + }, + { + "type": "Config", + "description": "Configs allow services to adapt their behaviour without the need to rebuild a Docker image. Services can only access configs when explicitly granted by a configs attribute within the services top-level element.", + "url": "https://docs.docker.com/compose/compose-file/08-configs/", + "model": "DefaultModel", + "icon": "config", + "displayName": "Config", + "isContainer": false, + "attributes": [ + { + "name": "file", + "type": "String", + "required": true + }, + { + "name": "name", + "type": "String" + }, + { + "name": "external", + "type": "Boolean" + } + ] + }, + { + "type": "Secret", + "description": "Secrets are a flavor of Configs focusing on sensitive data, with specific constraint for this usage.", + "url": "https://docs.docker.com/compose/compose-file/09-secrets/", + "model": "DefaultModel", + "icon": "secret", + "displayName": "Secret", + "isContainer": false, + "attributes": [ + { + "name": "file", + "type": "String", + "required": true + }, + { + "name": "name", + "type": "String" + }, + { + "name": "environment", + "type": "String" + }, + { + "name": "external", + "type": "Boolean" + } + ] + } +] \ No newline at end of file diff --git a/src/assets/metadata/index.js b/src/assets/metadata/index.js new file mode 100644 index 0000000..87d20ba --- /dev/null +++ b/src/assets/metadata/index.js @@ -0,0 +1,3 @@ +import jsonComponents from 'src/assets/metadata/docker-compose.json'; + +export default jsonComponents; diff --git a/src/configuration/syntax.js b/src/configuration/syntax.js new file mode 100644 index 0000000..db3976e --- /dev/null +++ b/src/configuration/syntax.js @@ -0,0 +1,259 @@ +export default { + name: 'docker-compose', + languageSettings: { + id: 'docker-compose', + extensions: ['.yaml', '.yml'], + aliases: ['docker-compose', 'docker compose'], + mimetypes: ['string'], + }, + languageConfiguration: { + comments: { + lineComment: '#', + blockComment: ['/*', '*/'], + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ], + colorizedBracketPairs: [ + ['{', '}'], + ['[', ']'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: "'", close: "'", notIn: ['string'] }, + { open: '"', close: '"', notIn: ['string'] }, + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: "'", close: "'" }, + { open: '"', close: '"' }, + ], + }, + tokenProvider: { + tokenPostfix: '.yaml', + + brackets: [ + { token: 'delimiter.bracket', open: '{', close: '}' }, + { token: 'delimiter.square', open: '[', close: ']' }, + ], + + keywords: [ + 'true', + 'True', + 'TRUE', + 'false', + 'False', + 'FALSE', + 'null', + 'Null', + 'Null', + '~', + ], + + numberInteger: /(?:0|[+-]?[0-9]+)/, + numberFloat: /(?:0|[+-]?[0-9]+)(?:\.[0-9]+)?(?:e[-+][1-9][0-9]*)?/, + numberOctal: /0o[0-7]+/, + numberHex: /0x[0-9a-fA-F]+/, + numberInfinity: /[+-]?\.(?:inf|Inf|INF)/, + numberNaN: /\.(?:nan|Nan|NAN)/, + numberDate: + /\d{4}-\d\d-\d\d([Tt ]\d\d:\d\d:\d\d(\.\d+)?(( ?[+-]\d\d?(:\d\d)?)|Z)?)?/, + + escapes: /\\(?:[btnfr\\'']|[0-7][0-7]?|[0-3][0-7]{2})/, + + tokenizer: { + root: [ + { include: '@whitespace' }, + { include: '@comment' }, + + // Directive + [/%[^ ]+.*$/, 'meta.directive'], + + // Document Markers + [/---/, 'operators.directivesEnd'], + [/\.{3}/, 'operators.documentEnd'], + + // Block Structure Indicators + [/[-?:](?= )/, 'operators'], + + { include: '@anchor' }, + { include: '@tagHandle' }, + { include: '@flowCollections' }, + { include: '@blockStyle' }, + + // Numbers + [/@numberInteger(?![ \t]*\S+)/, 'number'], + [/@numberFloat(?![ \t]*\S+)/, 'number.float'], + [/@numberOctal(?![ \t]*\S+)/, 'number.octal'], + [/@numberHex(?![ \t]*\S+)/, 'number.hex'], + [/@numberInfinity(?![ \t]*\S+)/, 'number.infinity'], + [/@numberNaN(?![ \t]*\S+)/, 'number.nan'], + [/@numberDate(?![ \t]*\S+)/, 'number.date'], + + // Key:Value pair + [ + /('.*?'|'.*?'|.*?)([ \t]*)(:)( |$)/, + ['type', 'white', 'operators', 'white'], + ], + + { include: '@flowScalars' }, + + // String nodes + [ + /.+$/, + { + cases: { + '@keywords': 'keyword', + '@default': 'string', + }, + }, + ], + ], + + // Flow Collection: Flow Mapping + object: [ + { include: '@whitespace' }, + { include: '@comment' }, + + // Flow Mapping termination + [/\}/, '@brackets', '@pop'], + + // Flow Mapping delimiter + [/,/, 'delimiter.comma'], + + // Flow Mapping Key:Value delimiter + [/:(?= )/, 'operators'], + + // Flow Mapping Key:Value key + [/(?:'.*?'|'.*?'|[^,{[]+?)(?=: )/, 'type'], + + // Start Flow Style + { include: '@flowCollections' }, + { include: '@flowScalars' }, + + // Scalar Data types + { include: '@tagHandle' }, + { include: '@anchor' }, + { include: '@flowNumber' }, + + // Other value (keyword or string) + [ + /[^},]+/, + { + cases: { + '@keywords': 'keyword', + '@default': 'string', + }, + }, + ], + ], + + // Flow Collection: Flow Sequence + array: [ + { include: '@whitespace' }, + { include: '@comment' }, + + // Flow Sequence termination + [/\]/, '@brackets', '@pop'], + + // Flow Sequence delimiter + [/,/, 'delimiter.comma'], + + // Start Flow Style + { include: '@flowCollections' }, + { include: '@flowScalars' }, + + // Scalar Data types + { include: '@tagHandle' }, + { include: '@anchor' }, + { include: '@flowNumber' }, + + // Other value (keyword or string) + [ + /[^\],]+/, + { + cases: { + '@keywords': 'keyword', + '@default': 'string', + }, + }, + ], + ], + + // Flow Scalars (quoted strings) + string: [ + [/[^\\'']+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [ + /['']/, + { + cases: { + '$#==$S2': { token: 'string', next: '@pop' }, + '@default': 'string', + }, + }, + ], + ], + + // First line of a Block Style + multiString: [[/^( +).+$/, 'string', '@multiStringContinued.$1']], + + // Further lines of a Block Style + // Workaround for indentation detection + multiStringContinued: [ + [ + /^( *).+$/, + { + cases: { + '$1==$S2': 'string', + '@default': { token: '@rematch', next: '@popall' }, + }, + }, + ], + ], + + whitespace: [[/[ \t\r\n]+/, 'white']], + + // Only line comments + comment: [[/#.*$/, 'comment']], + + // Start Flow Collections + flowCollections: [ + [/\[/, '@brackets', '@array'], + [/\{/, '@brackets', '@object'], + ], + + // Start Flow Scalars (quoted strings) + flowScalars: [ + [/"/, 'string', '@string."'], + [/'/, 'string', '@string.\''], + ], + + // Start Block Scalar + blockStyle: [[/[>|][0-9]*[+-]?$/, 'operators', '@multiString']], + + // Numbers in Flow Collections (terminate with ,]}) + flowNumber: [ + [/@numberInteger(?=[ \t]*[,\]}])/, 'number'], + [/@numberFloat(?=[ \t]*[,\]}])/, 'number.float'], + [/@numberOctal(?=[ \t]*[,\]}])/, 'number.octal'], + [/@numberHex(?=[ \t]*[,\]}])/, 'number.hex'], + [/@numberInfinity(?=[ \t]*[,\]}])/, 'number.infinity'], + [/@numberNaN(?=[ \t]*[,\]}])/, 'number.nan'], + [/@numberDate(?=[ \t]*[,\]}])/, 'number.date'], + ], + + tagHandle: [ + [/![^ ]*/, 'tag'], + ], + + anchor: [ + [/[&*][^ ]+/, 'namespace'], + ], + }, + }, +}; diff --git a/src/draw/DockerComposeDrawer.js b/src/draw/DockerComposeDrawer.js index bd48f99..c728458 100644 --- a/src/draw/DockerComposeDrawer.js +++ b/src/draw/DockerComposeDrawer.js @@ -23,7 +23,8 @@ class DockerComposeDrawer extends DefaultDrawer { ...options, minHeight: 80, minWidth: 110, - margin: 5, + margin: 10, + padding: 20, }); } } diff --git a/src/lidy/dockerComposeGrammar.js b/src/lidy/dockerComposeGrammar.js new file mode 100644 index 0000000..dd46ae0 --- /dev/null +++ b/src/lidy/dockerComposeGrammar.js @@ -0,0 +1,6 @@ +import { parse as parse_input } from 'lidy-js' +let rules={"main":"root","root":{"_oneOf":["docker-compose"]},"docker-compose":{"_map":{"version":"string"},"_mapFacultative":{"services":{"_mapOf":{"string":"service"}},"volumes":{"_mapOf":{"string":"volume"}},"networks":{"_mapOf":{"string":"network"}},"configs":{"_mapOf":{"string":"config"}},"secrets":{"_mapOf":{"string":"secret"}}}},"service":{"_map":{"image":"string"},"_mapFacultative":{"build":{"_mapFacultative":{"context":"string","dockerfile":"string","args":{"_listOf":"string"}}},"depends_on":{"_mapOf":{"string":{"_mapFacultative":{"condition":"string"}}}},"healthcheck":{"_mapFacultative":{"test":"string","interval":"string","timeout":"string","retries":"int"}},"networks":{"_listOf":"string"},"volumes":{"_listOf":"string"},"configs":{"_listOf":"string"},"secrets":{"_listOf":"string"},"stdin_open":"boolean","tty":"boolean","privileged":"boolean","command":"string","environment":{"_listOf":"string"},"ports":{"_listOf":"string"}}},"volume":{"_map":{"driver":"string"},"_mapFacultative":{"driver_opts":{"_listOf":"string"},"labels":{"_listOf":"string"},"external":"boolean"}},"network":{"_map":{"driver":"string"},"_mapFacultative":{"driver_opts":{"_listOf":"string"},"enable_ipv6":"boolean","ipam":{"_map":null,"driver":"string","_mapFacultative":{"subnet":"string","ip_range":"string","gateway":"string","aux_adresses":{"_listOf":"string"}}},"options":{"_listOf":"string"}}},"config":{"_map":{"file":"string"},"_mapFacultative":{"name":"string","external":"boolean"}},"secret":{"_map":{"file":"string"},"_mapFacultative":{"name":"string","environment":"string","external":"boolean"}}} +export function parse(input) { + input.rules = rules + return parse_input(input) +} \ No newline at end of file diff --git a/src/lidy/dockerComposeGrammar.yml b/src/lidy/dockerComposeGrammar.yml new file mode 100644 index 0000000..ce5061a --- /dev/null +++ b/src/lidy/dockerComposeGrammar.yml @@ -0,0 +1,109 @@ +main: root + +root: + _oneOf: + - docker-compose + + +docker-compose: + _map: + version: string + _mapFacultative: + services: + _mapOf: + string: service + volumes: + _mapOf: + string: volume + networks: + _mapOf: + string: network + configs: + _mapOf: + string: config + secrets: + _mapOf: + string: secret + +service: + _map: + image: string + _mapFacultative: + build: + _mapFacultative: + context: string + dockerfile: string + args: + _listOf: string + depends_on: + _mapOf: + string: + _mapFacultative: + condition: string + healthcheck: + _mapFacultative: + test: string + interval: string + timeout: string + retries: int + networks: + _listOf: string + volumes: + _listOf: string + configs: + _listOf: string + secrets: + _listOf: string + stdin_open: boolean + tty: boolean + privileged: boolean + command: string + environment: + _listOf: string + ports: + _listOf: string + +volume: + _map: + driver: string + _mapFacultative: + driver_opts: + _listOf: string + labels: + _listOf: string + external: boolean + + +network: + _map: + driver: string + _mapFacultative: + driver_opts: + _listOf: string + enable_ipv6: boolean + ipam: + _map: + driver: string + _mapFacultative: + subnet: string + ip_range: string + gateway: string + aux_adresses: + _listOf: string + options: + _listOf: string + +config: + _map: + file: string + _mapFacultative: + name: string + external: boolean + +secret: + _map: + file: string + _mapFacultative: + name: string + environment: string + external: boolean diff --git a/src/metadata/DockerComposeMetadata.js b/src/metadata/DockerComposeMetadata.js index d3bcf5a..0c9f578 100644 --- a/src/metadata/DockerComposeMetadata.js +++ b/src/metadata/DockerComposeMetadata.js @@ -1,24 +1,171 @@ -import { DefaultMetadata } from 'leto-modelizer-plugin-core'; +import Ajv from 'ajv'; +import { + DefaultMetadata, + ComponentDefinition, + ComponentAttributeDefinition, +} from 'leto-modelizer-plugin-core'; +import jsonComponents from 'src/assets/metadata'; +import Schema from 'src/metadata/ValidationSchema'; -/** - * Class to validate and retrieve component definitions from DockerCompose metadata. +/* + * Metadata is used to generate definitions of Components and ComponentAttributes. + * + * In our plugin managing Docker Composator, we use [Ajv](https://ajv.js.org/) to validate metadata. + * And we provide a `assets/metadata/docker-compose.json` to define all metadata. + * */ class DockerComposeMetadata extends DefaultMetadata { + /** + * Default constructor. + * @param {object} pluginData - Plugin data. + */ + constructor(pluginData) { + super(pluginData); + /** + * ajv. + * @type {Ajv} + */ + this.ajv = new Ajv(); + /** + * schema. + * @type {Schema} + */ + this.schema = Schema; + + /** + * dockerComosator components + * @type {jsonComponents} + */ + this.jsonComponents = jsonComponents; + + /** + * dockerComosator validation + * @type {validate} + */ + this.validate = this.validate.bind(this); + } + /** * Validate the provided metadata with a schema. * @returns {boolean} True if metadata is valid. */ validate() { + const errors = []; + const validate = this.ajv.compile(this.schema); + if (!validate(this.jsonComponents)) { + errors.push({ + ...this.jsonComponents, + errors: validate.errors, + }); + } + + if (errors.length > 0) { + return false; + } + return true; } /** - * Parse all component/link definitions from metadata. + * Function that adds component definitions from JSON file to pluginData. */ parse() { - this.pluginData.definitions = { - components: [], - }; + const componentDefs = jsonComponents.flatMap( + (component) => this.getComponentDefinition(component), + ); + this.setChildrenTypes(componentDefs); + this.pluginData.definitions.components.push(...componentDefs); + } + + /** + * Convert a JSON component definition object to a ComponentDefinition. + * @param {object} component - JSON component definition object to parse. + * @returns {ComponentDefinition} Parsed component definition. + */ + getComponentDefinition(component) { + const attributes = [...component.attributes]; + if (component.type !== 'Docker-Compose') { + // All components are children of the Docker-Compose, so they must have a reference attibute. + attributes.push({ + name: 'parentCompose', + type: 'Reference', + containerRef: 'Docker-Compose', + required: true, + }); + } + const definedAttributes = attributes.map(this.getAttributeDefinition, this); + const componentDef = new ComponentDefinition({ + ...component, + definedAttributes, + }); + componentDef.parentTypes = this.getParentTypes(componentDef); + return componentDef; + } + + /** + * Convert a JSON attribute object to a ComponentAttributeDefinition. + * @param {object} attribute - JSON attribute definition object to parse. + * @returns {ComponentAttributeDefinition} Parsed attribute definition. + */ + getAttributeDefinition(attribute) { + const subAttributes = attribute.attributes || []; + const attributeDef = new ComponentAttributeDefinition({ + ...attribute, + displayName: attribute.displayName || this.formatDisplayName(attribute.name), + definedAttributes: subAttributes.map(this.getAttributeDefinition, this), + }); + attributeDef.expanded = attribute.expanded || false; + return attributeDef; + } + + /** + * Format a name into a readable displayName. + * @param {string} name - Name to format. + * @returns {string} Formatted displayName. + */ + formatDisplayName(name) { + if (!name) { + return name; + } + const s = name.replace(/([A-Z])/g, ' $1'); + return s.charAt(0).toUpperCase() + s.slice(1); + } + + /** + * Get all possible parent container types. + * @param {DockerComposeComponentDefinition} componentDefinition - Definition to get all parent + * container types. + * @returns {string[]} All possible parent container types. + */ + getParentTypes(componentDefinition) { + const { parentTypes } = componentDefinition; + + componentDefinition.definedAttributes + .filter((attribute) => attribute.type === 'Reference') + .map((attribute) => attribute.containerRef) + .filter((ref) => !parentTypes.includes(ref)) + .forEach((ref) => parentTypes.push(ref)); + return parentTypes; + } + + /** + * Set the childrenTypes of all containers from children's parentTypes. + * @param {DockerComposeComponentDefinition[]} componentDefinitions - Array of + * component definitions. + */ + setChildrenTypes(componentDefinitions) { + const children = componentDefinitions + .filter((def) => def.parentTypes.length > 0) + .reduce((acc, def) => { + def.parentTypes.forEach((parentType) => { + acc[parentType] = [...(acc[parentType] || []), def.type]; + }); + return acc; + }, {}); + componentDefinitions.filter((def) => children[def.type]) + .forEach((def) => { + def.childrenTypes = children[def.type]; + }); } } diff --git a/src/metadata/ValidationSchema.js b/src/metadata/ValidationSchema.js new file mode 100644 index 0000000..52872e2 --- /dev/null +++ b/src/metadata/ValidationSchema.js @@ -0,0 +1,49 @@ +export default { + type: 'array', + items: { + type: 'object', + properties: { + type: { type: 'string' }, + model: { type: 'string' }, + displayName: { type: 'string' }, + icon: { type: 'string' }, + isContainer: { type: 'boolean' }, + childrenTypes: { + type: 'array', + items: { type: 'string' }, + }, + parentTypes: { + type: 'array', + items: { type: 'string' }, + }, + attributes: { + type: 'array', + items: { + $ref: '#/definitions/attribute', + }, + }, + }, + }, + definitions: { + attribute: { + type: 'object', + properties: { + name: { anyOf: [{ type: 'string' }, { type: 'null' }] }, + type: { + type: 'string', + pattern: '(String|Boolean|Number|Array|Object|Link|Reference)', + }, + required: { type: 'boolean' }, + containerRef: { type: 'string' }, + expanded: { type: 'boolean' }, + attributes: { + type: 'array', + items: { $ref: '#/definitions/attribute' }, + }, + linkRef: { type: 'string' }, + linkColor: { type: 'string' }, + }, + required: ['type'], + }, + }, +}; diff --git a/src/models/DockerComposeComponent.js b/src/models/DockerComposeComponent.js new file mode 100644 index 0000000..8db2bbc --- /dev/null +++ b/src/models/DockerComposeComponent.js @@ -0,0 +1,31 @@ +import { Component } from 'leto-modelizer-plugin-core'; + +/** + * Specific Docker compose component. + */ +class DockerComposeComponent extends Component { + /** + * Get attribute by name recursively. + * @param {ComponentAttribute[]} attributes - Array of component attributes. + * @param {string} name - Name of the attribute to search for. + * @returns {ComponentAttribute|null} Found attribute or null if not found. + * @private + */ + __getAttributeByName(attributes, name) { + for (let index = 0; index < attributes.length; index += 1) { + if (attributes[index].name === name) { + return attributes[index]; + } + if (attributes[index].type === 'Object' || attributes[index].type === 'Array') { + const attribute = this.__getAttributeByName(attributes[index].value, name); + if (attribute) { + return attribute; + } + } + } + + return null; + } +} + +export default DockerComposeComponent; diff --git a/src/models/DockerComposeConfiguration.js b/src/models/DockerComposeConfiguration.js index 63b7d5e..81a1a67 100644 --- a/src/models/DockerComposeConfiguration.js +++ b/src/models/DockerComposeConfiguration.js @@ -1,4 +1,5 @@ import { DefaultConfiguration, Tag } from 'leto-modelizer-plugin-core'; +import syntax from 'src/configuration/syntax'; /** * DockerCompose configuration. @@ -11,10 +12,9 @@ class DockerComposeConfiguration extends DefaultConfiguration { constructor(props) { super({ ...props, - defaultFileName: 'compose.yaml', - defaultFileExtension: 'yml', editor: { - ...props?.editor, + ...props.editor, + syntax, }, tags: [ new Tag({ type: 'language', value: 'DockerCompose' }), diff --git a/src/models/DockerComposeData.js b/src/models/DockerComposeData.js new file mode 100644 index 0000000..f66a676 --- /dev/null +++ b/src/models/DockerComposeData.js @@ -0,0 +1,112 @@ +import { + DefaultData, + ComponentLink, + ComponentLinkDefinition, +} from 'leto-modelizer-plugin-core'; +// import Component from 'src/models/DockerComposeComponent'; + +/** + * Specific Docker compose data. + * @augments {DefaultData} + */ +class DockerComposeData extends DefaultData { + /** + * Get all component links. + * @returns {ComponentLink[]} Array of component links. + */ + getLinks() { + const links = []; + + // Create depends_on links for Service Components + this.components.forEach((component) => { + const dependsOnAttribute = component.attributes.find( + ({ name }) => name === 'depends_on', + ); + if (dependsOnAttribute) { + dependsOnAttribute.value.forEach((item) => { + const definition = this.definitions.links.find( + ({ attributeRef }) => attributeRef === 'service', + ); + + links.push(new ComponentLink({ + definition, + source: component.id, + target: item.value.find( + ({ name }) => name.startsWith('service'), + ).value[0], + })); + }); + } + }); + + // Create volumes links for Service components + this.components.forEach((component) => { + const volumesAttribute = component.attributes.find( + ({ name }) => name === 'volumes', + ); + if (volumesAttribute) { + volumesAttribute.value.forEach((item) => { + const definition = this.definitions.links.find( + ({ attributeRef }) => attributeRef === 'volume-name', + ); + + links.push(new ComponentLink({ + definition, + source: component.id, + target: item.value.find( + ({ name }) => name.startsWith('volume'), + ).value[0], + })); + }); + } + }); + + // Create other links based on link definitions + this.definitions.links.forEach((definition) => { + const components = this.getComponentsByType(definition.sourceRef); + components.forEach((component) => { + const attribute = component.getAttributeByName(definition.attributeRef); + if (!attribute) { + return; + } + attribute.value.forEach((value) => { + links.push(new ComponentLink({ + definition, + source: component.id, + target: value, + })); + }); + }); + }); + + return links; + } + + /** + * Set link definition in link definitions. + * @param {string} type - Component type to link. + * @param {ComponentAttributeDefinition[]} definedAttributes - Component attribute definitions. + * @private + */ + __setLinkDefinitions(type, definedAttributes) { + definedAttributes.forEach((attributeDefinition) => { + if (attributeDefinition.type === 'Link') { + const linkDefinition = new ComponentLinkDefinition({ + type: attributeDefinition.linkType, + attributeRef: attributeDefinition.name, + sourceRef: type, + targetRef: attributeDefinition.linkRef, + color: attributeDefinition.linkColor, + width: attributeDefinition.linkWidth, + dashStyle: attributeDefinition.linkDashStyle, + }); + + this.definitions.links.push(linkDefinition); + } else if (attributeDefinition.type === 'Object' || attributeDefinition.type === 'Array') { + this.__setLinkDefinitions(type, attributeDefinition.definedAttributes); + } + }); + } +} + +export default DockerComposeData; diff --git a/src/models/DockerComposePlugin.js b/src/models/DockerComposePlugin.js index 023da82..41e990b 100644 --- a/src/models/DockerComposePlugin.js +++ b/src/models/DockerComposePlugin.js @@ -1,7 +1,7 @@ import { DefaultPlugin, - DefaultData, } from 'leto-modelizer-plugin-core'; +import DockerComposeData from 'src/models/DockerComposeData'; import DockerComposeDrawer from 'src/draw/DockerComposeDrawer'; import DockerComposeMetadata from 'src/metadata/DockerComposeMetadata'; import DockerComposeParser from 'src/parser/DockerComposeParser'; @@ -10,20 +10,23 @@ import DockerComposeConfiguration from 'src/models/DockerComposeConfiguration'; import packageInfo from 'package.json'; /** - * DockerCompose plugin. + * Docker compose plugin. */ class DockerComposePlugin extends DefaultPlugin { /** * Default constructor. - * @param {object} [props] - Object that contains all properties to set. - * @param {object} [props.event] - Event manager. - * @param {Function} [props.event.next] - Function to emit event. + * @param {object} props - Plugin properties. + * @param {string} props.event - Event data. */ constructor(props = { event: null, }) { - const configuration = new DockerComposeConfiguration(); - const pluginData = new DefaultData(configuration, { + const configuration = new DockerComposeConfiguration({ + defaultFileName: 'docker-compose.yaml', + defaultFileExtension: 'yaml', + }); + + const pluginData = new DockerComposeData(configuration, { name: packageInfo.name, version: packageInfo.version, }, props.event); diff --git a/src/parser/DockerComposeListener.js b/src/parser/DockerComposeListener.js new file mode 100644 index 0000000..7ac81cf --- /dev/null +++ b/src/parser/DockerComposeListener.js @@ -0,0 +1,380 @@ +import { ComponentAttribute, ComponentAttributeDefinition } from 'leto-modelizer-plugin-core'; +import DockerComposeComponent from 'src/models/DockerComposeComponent'; + +/** + * Lidy listener for Docker Compose files. + */ +class DockerComposeListener { + /** + * Default constructor. + * @param {FileInformation} fileInformation - File information. + * @param {ComponentDefinition[]} [definitions=[]] - All component definitions. + */ + constructor(fileInformation, definitions = []) { + /** + * fileInformation + * @param {object} fileInformation + */ + this.fileInformation = fileInformation; + /** + * definitions + * @param {definitions} fileInformation + */ + this.definitions = definitions; + /** + * components + * @param {Array} components + */ + this.components = []; + /** + * childComponentsByType + * @param {object} childComponentsByType + */ + this.childComponentsByType = {}; + } + + /** + * Function called when attribute `root` is parsed. + * Create a component from the parsed root element. + * @param {MapNode} rootNode - The Lidy `root` node. + */ + exit_root(rootNode) { + let type = ''; + if (rootNode.value.version) { + type = 'Docker-Compose'; + const rootComponent = this.createComponentFromTree(rootNode, type); + rootComponent.path = this.fileInformation.path; + rootComponent.definition.childrenTypes.forEach((childType) => { + if (!this.childComponentsByType[childType]) { + this.childComponentsByType[childType] = []; + } + this.setParentComponent(rootComponent, this.childComponentsByType[childType].filter( + (component) => component.path === rootComponent.path, + )); + }); + } + } + + /** + * Function called when parsing a service. + * Create a component from the parsed service element. + * @param {MapNode} serviceNode - The Lidy `service` node. + */ + exit_service(serviceNode) { + this.createComponent(serviceNode, 'Service'); + } + + /** + * Function called when parsing a volume. + * Create a component from the parsed volume element. + * @param {MapNode} volumeNode - The Lidy `volume` node. + */ + exit_volume(volumeNode) { + this.createComponent(volumeNode, 'Volume'); + } + + /** + * Function called when parsing a network. + * Create a component from the parsed network element. + * @param {MapNode} networkNode - The Lidy `network` node. + */ + exit_network(networkNode) { + this.createComponent(networkNode, 'Network'); + } + + /** + * Function called when parsing a config. + * Create a component from the parsed config element. + * @param {MapNode} configNode - The Lidy `config` node. + */ + exit_config(configNode) { + this.createComponent(configNode, 'Config'); + } + + /** + * Function called when parsing a secret. + * Create a component from the parsed secret element. + * @param {MapNode} secretNode - The Lidy `secret` node. + */ + exit_secret(secretNode) { + this.createComponent(secretNode, 'Secret'); + } + + /** + * Function called to create component on exit. + * @param {MapNode} node - The node that contains the tree. + * @param {string} type - The type of the component that will be created. + */ + createComponent(node, type) { + if (node) { + const component = this.createComponentFromTree(node, type); + component.path = this.fileInformation.path; + if (!this.childComponentsByType[type]) { + this.childComponentsByType[type] = []; + } + this.childComponentsByType[type].push(component); + } + } + + /** + * Function called to create id for component based on name. + * @param {MapNode} node - The node that contains the tree. + * @param {string} type - The type of the component that will be created. + * @returns {string | null} - The created id or null if type is not supported + */ + setComponentId(node, type) { + let id = 'unnamed_component'; + // If the component is Docker-Compose, set the id as the name of the file + // else find the name from the list of components respective to type + switch (type) { + case 'Docker-Compose': + { + id = this.fileInformation.path?.split('/').pop().split('.')[0]; + break; + } + case 'Service': + case 'Volume': + case 'Network': + case 'Config': + case 'Secret': + { + const nodeObject = JSON.parse(JSON.stringify(node.ctx.src)); + const typeLowerCase = type.toLowerCase(); + const srcMap = new Map(Object.entries(nodeObject[`${typeLowerCase}s`])); + + const currentString = JSON.stringify(node.current); + srcMap.forEach((value, key) => { + if (JSON.stringify(value) === currentString) { + id = key; + } + }); + break; + } + default: + return null; + } + return id; + } + + /** + * Function called to create component from its tree. + * @param {MapNode} node - The node that contains the tree. + * @param {string} type - The type of the component that will be created. + * @returns {DockerComposeComponent} - The constructed Component + */ + createComponentFromTree(node, type) { + const definition = this.definitions.find((def) => def.type === type); + const id = this.setComponentId(node, type); + if (!id) { + return null; + } + if (type === 'Docker-Compose') { + const propsToDelete = ['services', 'volumes', 'networks', 'secrets', 'configs']; + propsToDelete.forEach((prop) => delete node.value[prop]); + } + + delete node?.value?.metadata?.value.name; + delete node?.value?.name; + + const component = new DockerComposeComponent({ + id, + definition, + attributes: this.createAttributesFromTreeNode(id, node, definition), + }); + + this.components.push(component); + return component; + } + + /** + * Function called to create component attributes from component tree. + * @param {string} id - The id of the component. + * @param {MapNode} parentNode - The node that contains the tree. + * @param {ComponentDefinition} parentDefinition - The definition of the component. + * @returns {ComponentAttribute} - The constructed ComponentAttribute. + */ + createAttributesFromTreeNode(id, parentNode, parentDefinition) { + const result = Object.keys(parentNode.value).map((childKey) => { + const childNode = parentNode.value[childKey]; + const definition = parentDefinition?.definedAttributes.find( + ({ name }) => name === (parentNode.type !== 'list' ? childKey : null), + ); + + let attributeValue = {}; + if (childKey === 'depends_on') { + return this.createDependsOnAttribute(id, childNode, definition); + } + if (childKey === 'volumes') { + return this.createVolumesAttribute(id, childNode, definition); + } + if ((childNode.type === 'map' || childNode.type === 'list')) { + attributeValue = this.createAttributesFromTreeNode(id, childNode, definition); + } else if (childNode.type === 'string' && (!childKey || /[0-9]+/i.test(childKey))) { + return childNode.value; + } else { + attributeValue = childNode.value; + } + + const attribute = new ComponentAttribute({ + name: childKey, + type: this.lidyToLetoType(childNode.type), + definition, + value: attributeValue, + }); + + return attribute; + }); + + return result; + } + + /** + * Function called to set parent component id to child. + * @param {DockerComposeComponent} parentComponent - The parent component (docker-compose) + * whose reference will be added to the children. + * @param {DockerComposeComponent[]} childComponents - The child components who will receive + * the reference to the parent. + */ + setParentComponent(parentComponent, childComponents) { + childComponents?.forEach((childComponent) => { + childComponent.setReferenceAttribute(parentComponent); + }); + } + + /** + * Function called to create volumes attribute from component tree. + * @param {string} id - The id of the component. + * @param {MapNode} childNode - The node that contains the tree. + * @param {ComponentDefinition} definition - The definition of the component. + * @returns {ComponentAttribute} - The constructed volumes ComponentAttribute. + */ + createVolumesAttribute(id, childNode, definition) { + const volumesAttributeValue = []; + childNode.childs.forEach((child, i) => { + // Fetch the definition of the link from volumes attributes definitions + const linkDefinition = definition.definedAttributes[0].definedAttributes.find( + ({ type }) => type === 'Link', + ); + + // Set the name for the new link to be created and add its definition from + // the one fetched previously. The name is based on the id of the current component and + // the index of the link (child). + const newLinkName = `volume_${id}_${i}`; + const newLinkAttributeDefinition = new ComponentAttributeDefinition({ + ...linkDefinition, + name: newLinkName, + }); + // Since volumes is an array of multiple objects (which are themselves ComponentAttributes) + // each containing a link and a condition, we push in this step the created object with its + // two attributes to the value of volumes. + volumesAttributeValue.push(new ComponentAttribute({ + name: null, + type: 'Object', + value: [ + new ComponentAttribute({ + name: newLinkName, + type: 'Array', + definition: newLinkAttributeDefinition, + value: [child.value.split(':')[0]], + }), + new ComponentAttribute({ + name: 'mount-path', + type: 'String', + definition: definition.definedAttributes[0].definedAttributes.find( + ({ name }) => name === 'mount-path', + ), + value: child.value.split(':')[1], + }), + ], + })); + }); + return new ComponentAttribute({ + name: 'volumes', + type: 'Array', + definition, + value: volumesAttributeValue, + }); + } + + /** + * Function called to create depends_on attribute from component tree. + * @param {string} id - The id of the component. + * @param {MapNode} childNode - The node that contains the tree. + * @param {ComponentDefinition} definition - The definition of the component. + * @returns {ComponentAttribute} - The constructed depends_on ComponentAttribute. + */ + createDependsOnAttribute(id, childNode, definition) { + const dependsOnValue = []; + childNode.childs.forEach((child, i) => { + // Fetch the definition of the link from depends_on attributes definitions + const linkDefinition = definition.definedAttributes[0].definedAttributes.find( + ({ type }) => type === 'Link', + ); + + // Set the name for the new link to be created and add its definition from + // the one fetched previously. The name is based on the id of the current component and + // the index of the link (child). + const newLinkName = `service_${id}_${i}`; + const newLinkAttributeDefinition = new ComponentAttributeDefinition({ + ...linkDefinition, + name: newLinkName, + }); + // Since depends_on is an array of multiple objects (which are themselves ComponentAttributes) + // each containing a link and a condition, we push in this step the created object with its + // two attributes to the value of depends_on. + dependsOnValue.push(new ComponentAttribute({ + name: null, + type: 'Object', + value: [ + new ComponentAttribute({ + name: newLinkName, + type: 'Array', + definition: newLinkAttributeDefinition, + value: [child.key.value], + }), + new ComponentAttribute({ + name: 'condition', + type: 'String', + definition: definition.definedAttributes[0].definedAttributes.find( + ({ name }) => name === 'condition', + ), + value: child.value.condition.value, + }), + ], + })); + }); + + return new ComponentAttribute({ + name: 'depends_on', + type: 'Array', + definition, + value: dependsOnValue, + }); + } + + /** + * Function called to convert lidy type to Leto type. + * @param {string} lidyType - The lidy attribute type that must be converted. + * @returns {string | null} - The corresponding Leto type or null if + * lidyType isn't one from the list. + */ + lidyToLetoType(lidyType) { + switch (lidyType) { + case 'string': + return 'String'; + case 'boolean': + return 'Boolean'; + case 'int': + case 'float': + return 'Number'; + case 'map': + return 'Object'; + case 'list': + return 'Array'; + default: + return null; + } + } +} + +export default DockerComposeListener; diff --git a/src/parser/DockerComposeParser.js b/src/parser/DockerComposeParser.js index 70a9119..aabd24e 100644 --- a/src/parser/DockerComposeParser.js +++ b/src/parser/DockerComposeParser.js @@ -1,31 +1,89 @@ import { DefaultParser } from 'leto-modelizer-plugin-core'; +import { parse as lidyParse } from 'src/lidy/dockerComposeGrammar'; +import DockerComposeListener from 'src/parser/DockerComposeListener'; /** - * Class to parse and retrieve components/links from DockerCompose files. + * Class to parse and retrieve components/links from Docker compose files. */ class DockerComposeParser extends DefaultParser { /** - * Indicate if this parser can parse this file. - * @returns {boolean} Boolean that indicates if this file can be parsed or not. + * Default constructor. + * @param {PluginData} [pluginData] - pluginData - Plugin data with components */ - isParsable() { - return false; + constructor(pluginData) { + super(pluginData); + /** + * listener + * @param {DockerComposeListener} listener + */ + this.listener = new DockerComposeListener(); } /** - * Get the list of model paths from all files. - * @returns {string[]} List of folder paths that represent a model. + * Indicate if this parser can parse this file. + * @param {FileInformation} [fileInformation] - File information. + * @returns {boolean} Boolean that indicates if this file can be parsed or not. */ - getModels() { - return []; + isParsable(fileInformation) { + return /\.ya?ml$/.test(fileInformation.path); } /** * Convert the content of files into Components. + * @param {FileInformation} diagram - Diagram file information. + * @param {FileInput[]} [inputs=[]] - Data you want to parse. + * @param {string} [parentEventId=null] - Parent event id. */ - parse() { + parse(diagram, inputs = [], parentEventId = null) { this.pluginData.components = []; this.pluginData.parseErrors = []; + + inputs.filter(({ content, path }) => { + if (diagram.path === path && content && content.trim() !== '') { + return true; + } + this.pluginData.emitEvent({ + parent: parentEventId, + type: 'Parser', + action: 'read', + status: 'warning', + files: [path], + data: { + code: 'no_content', + global: false, + }, + }); + return false; + }).forEach((input) => { + const id = this.pluginData.emitEvent({ + parent: parentEventId, + type: 'Parser', + action: 'read', + status: 'running', + files: [input.path], + data: { + global: false, + }, + }); + this.listener.fileInformation = input; + this.listener.definitions = this.pluginData.definitions.components; + this.listener.components = this.pluginData.components; + lidyParse({ + src_data: input.content, + listener: this.listener, + path: input.path, + prog: { + errors: [], + warnings: [], + imports: [], + alreadyImported: [], + root: [], + }, + }); + this.listener.childComponentsByType = {}; + + this.pluginData.emitEvent({ id, status: 'success' }); + }); } } diff --git a/tests/resources/metadata/getComposatorMetadata.js b/tests/resources/metadata/getComposatorMetadata.js new file mode 100644 index 0000000..2f49ffa --- /dev/null +++ b/tests/resources/metadata/getComposatorMetadata.js @@ -0,0 +1,17 @@ +import fs from 'fs'; +import DockerComposeMetadata from 'src/metadata/DockerComposeMetadata'; +import DockerComposeData from 'src/models/DockerComposeData'; + +/** + * Create metadata from a specific metadata JSON file. + * @param {string} metadataName - metadata name. + * @param {string} metadataUrl - path to metadata JSON file. + * @returns {DockerComposeMetadata} DockerComposeMetadata instance containing metadata + * from specified url. + */ +export function getComposatorMetadata(metadataName, metadataUrl) { + const metadata = JSON.parse(fs.readFileSync(metadataUrl, 'utf8')); + const dockerComposatorPluginMetadata = new DockerComposeMetadata(new DockerComposeData()); + dockerComposatorPluginMetadata.jsonComponents = metadata; + return dockerComposatorPluginMetadata; +} diff --git a/tests/resources/metadata/invalid.json b/tests/resources/metadata/invalid.json new file mode 100644 index 0000000..7ecffdf --- /dev/null +++ b/tests/resources/metadata/invalid.json @@ -0,0 +1,3 @@ +{ + "type" : "invalid" +} \ No newline at end of file diff --git a/tests/resources/metadata/valid.json b/tests/resources/metadata/valid.json new file mode 100644 index 0000000..2b65e5e --- /dev/null +++ b/tests/resources/metadata/valid.json @@ -0,0 +1,353 @@ +[ + { + "type": "Docker-Compose", + "model": "DefaultContainer", + "displayName": "Docker Compose", + "icon": "compose", + "isContainer": true, + "attributes": [ + { + "name": "version", + "type": "String", + "required": true + } + ] + }, + { + "type": "Service", + "model": "DefaultModel", + "icon": "service", + "displayName": "Service", + "isContainer": false, + "attributes": [ + { + "name": "image", + "type": "String", + "required": true + }, + { + "name": "build", + "type": "Object", + "expanded": true, + "attributes": [ + { + "name": "context", + "type": "String" + }, + { + "name": "dockerfile", + "type": "String" + }, + { + "name": "args", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + } + ] + }, + { + "name": "depends_on", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "Object", + "attributes": [ + { + "name": "service", + "type": "Link", + "linkRef": "Service", + "linkColor": "red" + }, + { + "name": "condition", + "type": "String" + } + ] + } + ] + }, + { + "name": "environment", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "ports", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "healthcheck", + "type": "Object", + "expanded": true, + "attributes": [ + { + "name": "test", + "type": "String" + }, + { + "name": "interval", + "type": "String" + }, + { + "name": "timeout", + "type": "String" + }, + { + "name": "retries", + "type": "Number" + } + ] + }, + { + "name": "networks", + "type": "Link", + "linkRef": "Network", + "linkColor": "purple" + }, + { + "name": "volumes", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "Object", + "attributes": [ + { + "name": "volume-name", + "type": "Link", + "linkRef": "Volume", + "linkColor": "green" + }, + { + "name": "mount-path", + "type": "String" + } + ] + } + ] + }, + { + "name": "configs", + "type": "Link", + "linkRef": "Config", + "linkColor": "blue" + }, + { + "name": "secrets", + "type": "Link", + "linkRef": "Secret", + "linkColor": "pink" + }, + { + "name": "command", + "type": "String" + }, + { + "name": "stdin_open", + "type": "Boolean" + }, + { + "name": "privileged", + "type": "Boolean" + }, + { + "name": "tty", + "type": "Boolean" + } + ] + }, + { + "type": "Volume", + "model": "DefaultModel", + "icon": "volume", + "displayName": "Volume", + "isContainer": false, + "attributes": [ + { + "name": "driver", + "type": "String", + "required": true + }, + { + "name": "driver_opts", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "labels", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "external", + "type": "Boolean" + } + ] + }, + { + "type": "Network", + "model": "DefaultModel", + "icon": "network", + "displayName": "Network", + "isContainer": false, + "attributes": [ + { + "name": "driver", + "type": "String", + "required": true + }, + { + "name": "driver_opts", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "enable_ipv6", + "type": "Boolean" + }, + { + "name": "ipam", + "type": "Object", + "expanded": true, + "attributes": [ + { + "name": "driver", + "type": "String" + }, + { + "name": "config", + "type": "Object", + "attributes": [ + { + "name": "subnet", + "type": "String" + }, + { + "name": "ip_range", + "type": "String" + }, + { + "name": "gateway", + "type": "Array" + }, + { + "name": "aux_adresses", + "type": "Array", + "attributes": [ + { + "name": "host", + "type": "String" + } + ] + } + ] + }, + { + "name": "options", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + } + ] + }, + { + "name": "labels", + "type": "Array", + "attributes": [ + { + "name": null, + "type": "String" + } + ] + }, + { + "name": "external", + "type": "Boolean" + } + ] + }, + { + "type": "Config", + "model": "DefaultModel", + "icon": "config", + "displayName": "Config", + "isContainer": false, + "attributes": [ + { + "name": "file", + "type": "String", + "required": true + }, + { + "name": "name", + "type": "String" + }, + { + "name": "external", + "type": "Boolean" + } + ] + }, + { + "type": "Secret", + "model": "DefaultModel", + "icon": "secret", + "displayName": "Secret", + "isContainer": false, + "attributes": [ + { + "name": "file", + "type": "String", + "required": true + }, + { + "name": "name", + "type": "String" + }, + { + "name": "environment", + "type": "String" + }, + { + "name": "external", + "type": "Boolean" + } + ] + } +] \ No newline at end of file diff --git a/tests/resources/models/DataGetLinkTests.js b/tests/resources/models/DataGetLinkTests.js new file mode 100644 index 0000000..4a2b923 --- /dev/null +++ b/tests/resources/models/DataGetLinkTests.js @@ -0,0 +1,180 @@ +import { ComponentAttribute } from 'leto-modelizer-plugin-core'; +import DockerComposeData from 'src/models/DockerComposeData'; +import DockerComposeComponent from 'src/models/DockerComposeComponent'; +import DockerComposeMetadata from 'src/metadata/DockerComposeMetadata'; + +const pluginData = new DockerComposeData(); +const metadata = new DockerComposeMetadata(pluginData); +metadata.parse(); + +// Component definitions +const dockerComposeDef = pluginData.definitions.components + .find(({ type }) => type === 'Docker-Compose'); +const serviceDef = pluginData.definitions.components + .find(({ type }) => type === 'Service'); +const networkDef = pluginData.definitions.components + .find(({ type }) => type === 'Network'); +const volumeDef = pluginData.definitions.components + .find(({ type }) => type === 'Volume'); + +// Common attributes definitions +const serviceImageAttributeDef = serviceDef.definedAttributes.find(({ name }) => name === 'image'); +const dependsOnLinkDef = serviceDef.definedAttributes + .find(({ name }) => name === 'depends_on').definedAttributes[0].definedAttributes + .find(({ type }) => type === 'Link'); + +const parentComposeAttribute = new ComponentAttribute({ + name: 'parentCompose', + type: 'String', + definition: serviceDef.definedAttributes + .find(({ name }) => name === 'parentCompose'), + value: 'veto-full-compose', +}); + +// Instantiating components +const dockerCompose = new DockerComposeComponent({ + id: 'veto-full-compose', + path: './veto-full-compose.yaml', + definition: dockerComposeDef, + attributes: [ + new ComponentAttribute({ + name: 'version', + type: 'String', + definition: dockerComposeDef.definedAttributes + .find(({ name }) => name === 'version'), + value: '3.9', + }), + ], +}); + +const veterinaryConfigServerService = new DockerComposeComponent({ + id: 'veterinary-config-server', + path: './veto-full-compose.yaml', + definition: serviceDef, + attributes: [ + new ComponentAttribute({ + name: 'image', + type: 'String', + definition: serviceImageAttributeDef, + value: 'veterinary-config-server:0.2', + }), + new ComponentAttribute({ + name: 'networks', + type: 'Array', + definition: serviceDef.definedAttributes + .find(({ name }) => name === 'networks'), + value: ['backend'], + }), + new ComponentAttribute({ ...parentComposeAttribute }), + + ], +}); + +const veterinaryMsService = new DockerComposeComponent({ + id: 'veterinary-ms', + path: './veto-full-compose.yaml', + definition: serviceDef, + attributes: [ + new ComponentAttribute({ + name: 'image', + type: 'String', + definition: serviceImageAttributeDef, + value: 'veterinary-ms:0.2', + }), + new ComponentAttribute({ + name: 'depends_on', + type: 'Array', + definition: serviceDef.definedAttributes.find(({ name }) => name === 'depends_on'), + value: [ + new ComponentAttribute({ + name: null, + type: 'Object', + value: [ + new ComponentAttribute({ + name: 'service_veterinary-ms_0', + type: 'Array', + definition: dependsOnLinkDef, + value: ['veterinary-config-server'], + }), + new ComponentAttribute({ + name: 'condition', + type: 'String', + value: 'service healthy', + }), + ], + }), + ], + }), + new ComponentAttribute({ + name: 'volumes', + type: 'Array', + definition: serviceDef.definedAttributes.find(({ name }) => name === 'volumes'), + value: [ + new ComponentAttribute({ + name: null, + type: 'Object', + value: [ + new ComponentAttribute({ + name: 'volume_veterinary-ms_0', + type: 'Array', + value: ['data'], + }), + new ComponentAttribute({ + name: 'mount-path', + type: 'String', + value: 'service healthy', + }), + ], + }), + ], + }), + new ComponentAttribute({ ...parentComposeAttribute }), + + ], +}); + +const backendNetwork = new DockerComposeComponent({ + id: 'backend', + path: './veto-full-compose.yaml', + definition: networkDef, + attributes: [ + new ComponentAttribute({ + name: 'driver', + type: 'String', + definition: networkDef.definedAttributes + .find(({ name }) => name === 'driver'), + value: 'custom-driver-0', + }), + new ComponentAttribute({ ...parentComposeAttribute }), + + ], +}); + +const dataVolume = new DockerComposeComponent({ + id: 'data', + path: './veto-full-compose.yaml', + definition: volumeDef, + attributes: [ + new ComponentAttribute({ + name: 'driver', + type: 'String', + definition: volumeDef.definedAttributes + .find(({ name }) => name === 'driver'), + value: 'custom-driver', + }), + new ComponentAttribute({ ...parentComposeAttribute }), + + ], +}); + +// Adding components to pluginData +pluginData.components.push(dockerCompose); +pluginData.components.push(backendNetwork); +pluginData.components.push(dataVolume); +pluginData.components.push(veterinaryConfigServerService); +pluginData.components.push(veterinaryMsService); + +// Invoke the setLinkDefinitions method to generate the links +pluginData.__setLinkDefinitions('Service', serviceDef.definedAttributes); + +export default pluginData; diff --git a/tests/resources/parser/compose-with-simple-children.yaml b/tests/resources/parser/compose-with-simple-children.yaml new file mode 100644 index 0000000..e8fdc8e --- /dev/null +++ b/tests/resources/parser/compose-with-simple-children.yaml @@ -0,0 +1,16 @@ +version: '3.9' +services: + test-service: + image: busybox +networks: + backend: + driver: custom-driver-0 +volumes: + data: + driver: custom-driver-1 +secrets: + secret-file: + file: 'path/to/secret/file' +configs: + config-file: + file: 'path/to/config/file' \ No newline at end of file diff --git a/tests/resources/parser/empty-compose.js b/tests/resources/parser/empty-compose.js new file mode 100644 index 0000000..eb1b77e --- /dev/null +++ b/tests/resources/parser/empty-compose.js @@ -0,0 +1,30 @@ +import { ComponentAttribute } from 'leto-modelizer-plugin-core'; +import DockerComposeData from 'src/models/DockerComposeData'; +import DockerComposeComponent from 'src/models/DockerComposeComponent'; +import DockerComposeMetadata from 'src/metadata/DockerComposeMetadata'; + +const pluginData = new DockerComposeData(); +const metadata = new DockerComposeMetadata(pluginData); +metadata.parse(); + +const dockerComposeDef = pluginData.definitions.components + .find(({ type }) => type === 'Docker-Compose'); + +const dockerCompose = new DockerComposeComponent({ + id: 'empty-compose', + path: './empty-compose.yaml', + definition: dockerComposeDef, + attributes: [ + new ComponentAttribute({ + name: 'version', + type: 'String', + definition: dockerComposeDef.definedAttributes + .find(({ name }) => name === 'version'), + value: '3.9', + }), + ], +}); + +pluginData.components.push(dockerCompose); + +export default pluginData; diff --git a/tests/resources/parser/empty-compose.yaml b/tests/resources/parser/empty-compose.yaml new file mode 100644 index 0000000..85a60df --- /dev/null +++ b/tests/resources/parser/empty-compose.yaml @@ -0,0 +1 @@ +version: '3.9' \ No newline at end of file diff --git a/tests/resources/parser/veto-full-compose.js b/tests/resources/parser/veto-full-compose.js new file mode 100644 index 0000000..1e7449c --- /dev/null +++ b/tests/resources/parser/veto-full-compose.js @@ -0,0 +1,391 @@ +import { ComponentAttribute, ComponentAttributeDefinition } from 'leto-modelizer-plugin-core'; +import DockerComposeData from 'src/models/DockerComposeData'; +import DockerComposeComponent from 'src/models/DockerComposeComponent'; +import DockerComposeMetadata from 'src/metadata/DockerComposeMetadata'; + +const pluginData = new DockerComposeData(); +const metadata = new DockerComposeMetadata(pluginData); +metadata.parse(); + +// Components Definitions +const dockerComposeDef = pluginData.definitions.components + .find(({ type }) => type === 'Docker-Compose'); +const serviceDef = pluginData.definitions.components + .find(({ type }) => type === 'Service'); +const networkDef = pluginData.definitions.components + .find(({ type }) => type === 'Network'); +const volumeDef = pluginData.definitions.components + .find(({ type }) => type === 'Volume'); +const secretDef = pluginData.definitions.components + .find(({ type }) => type === 'Secret'); +const configDef = pluginData.definitions.components + .find(({ type }) => type === 'Config'); + +// Common attributes definitions +const serviceImageAttributeDef = serviceDef.definedAttributes + .find(({ name }) => name === 'image'); +const serviceNetworksAttributeDef = serviceDef.definedAttributes + .find(({ name }) => name === 'networks'); +const serviceBuildAttributeDef = serviceDef.definedAttributes + .find(({ name }) => name === 'build'); +const buildContextAttributeDef = serviceBuildAttributeDef.definedAttributes + .find(({ name }) => name === 'context'); +const serviceHealthcheckAttributeDef = serviceDef.definedAttributes + .find(({ name }) => name === 'healthcheck'); + +const parentComposeAttribute = new ComponentAttribute({ + name: 'parentCompose', + type: 'String', + definition: serviceDef.definedAttributes + .find(({ name }) => name === 'parentCompose'), + value: 'veto-full-compose', +}); + +// "Depends On" attribute special definitions +const dependsOnAttributeDef = serviceDef.definedAttributes + .find(({ name }) => name === 'depends_on'); +const dependsOnLinkDef = dependsOnAttributeDef.definedAttributes[0].definedAttributes + .find(({ type }) => type === 'Link'); +const dependsOnConditionDef = dependsOnAttributeDef.definedAttributes[0].definedAttributes + .find(({ name }) => name === 'condition'); +const dependsOnConfigServerDef = new ComponentAttributeDefinition({ + ...dependsOnLinkDef, + name: 'service_veterinary-ms_0', +}); +const dependsOnDatabaseDef = new ComponentAttributeDefinition({ + ...dependsOnLinkDef, + name: 'service_veterinary-ms_1', +}); + +// "Volumes" attribute special definitions +const serviceVolumesAttributeDef = serviceDef.definedAttributes + .find(({ name }) => name === 'volumes'); +const volumesLinkDef = serviceVolumesAttributeDef.definedAttributes[0].definedAttributes + .find(({ type }) => type === 'Link'); +const volumesMountpathDef = serviceVolumesAttributeDef.definedAttributes[0].definedAttributes + .find(({ name }) => name === 'mount-path'); +const volumesMsLinkDef = new ComponentAttributeDefinition({ + ...volumesLinkDef, + name: 'volume_database_0', +}); + +// Instantiating Components +const dockerCompose = new DockerComposeComponent({ + id: 'veto-full-compose', + path: './veto-full-compose.yaml', + definition: dockerComposeDef, + attributes: [ + new ComponentAttribute({ + name: 'version', + type: 'String', + definition: dockerComposeDef.definedAttributes + .find(({ name }) => name === 'version'), + value: '3.9', + }), + ], +}); + +const databaseService = new DockerComposeComponent({ + id: 'database', + path: './veto-full-compose.yaml', + definition: serviceDef, + attributes: [ + new ComponentAttribute({ + name: 'image', + type: 'String', + definition: serviceImageAttributeDef, + value: 'postgres', + }), + new ComponentAttribute({ + name: 'environment', + type: 'Array', + definition: serviceDef.definedAttributes + .find(({ name }) => name === 'environment'), + value: ['POSTGRES_USER=admin'], + }), + new ComponentAttribute({ + name: 'ports', + type: 'Array', + definition: serviceDef.definedAttributes + .find(({ name }) => name === 'ports'), + value: ['5432:5432'], + }), + new ComponentAttribute({ + name: 'networks', + type: 'Array', + definition: serviceNetworksAttributeDef, + value: ['backend'], + }), + new ComponentAttribute({ + name: 'volumes', + type: 'Array', + definition: serviceVolumesAttributeDef, + value: [new ComponentAttribute({ + name: null, + type: 'Object', + value: [ + new ComponentAttribute({ + name: 'volume_database_0', + type: 'Array', + definition: volumesMsLinkDef, + value: ['data'], + }), + new ComponentAttribute({ + name: 'mount-path', + type: 'String', + definition: volumesMountpathDef, + value: '/path/to/mount', + }), + ], + })], + }), + new ComponentAttribute({ + name: 'secrets', + type: 'Array', + definition: serviceDef.definedAttributes + .find(({ name }) => name === 'secrets'), + value: ['secret-file'], + }), + new ComponentAttribute({ + name: 'configs', + type: 'Array', + definition: serviceDef.definedAttributes + .find(({ name }) => name === 'configs'), + value: ['config-file'], + }), + new ComponentAttribute({ ...parentComposeAttribute }), + ], +}); + +const veterinaryConfigServerService = new DockerComposeComponent({ + id: 'veterinary-config-server', + path: './veto-full-compose.yaml', + definition: serviceDef, + attributes: [ + new ComponentAttribute({ + name: 'image', + type: 'String', + definition: serviceImageAttributeDef, + value: 'veterinary-config-server:0.2', + }), + new ComponentAttribute({ + name: 'build', + type: 'Object', + definition: serviceBuildAttributeDef, + value: [ + new ComponentAttribute({ + name: 'context', + type: 'String', + definition: buildContextAttributeDef, + value: './Backend/config-server', + }), + new ComponentAttribute({ + name: 'dockerfile', + type: 'String', + definition: serviceBuildAttributeDef.definedAttributes.find( + ({ name }) => name === 'dockerfile', + ), + value: './Backend/config-server/Dockerfile', + }), + ], + }), + new ComponentAttribute({ + name: 'healthcheck', + type: 'Object', + definition: serviceHealthcheckAttributeDef, + value: [ + new ComponentAttribute({ + name: 'test', + type: 'String', + definition: serviceHealthcheckAttributeDef.definedAttributes.find( + ({ name }) => name === 'test', + ), + value: 'curl -f http://localhost:2001/actuator/health', + }), + new ComponentAttribute({ + name: 'interval', + type: 'String', + definition: serviceHealthcheckAttributeDef.definedAttributes.find( + ({ name }) => name === 'interval', + ), + value: '30s', + }), + new ComponentAttribute({ + name: 'timeout', + type: 'String', + definition: serviceHealthcheckAttributeDef.definedAttributes.find( + ({ name }) => name === 'timeout', + ), + value: '5s', + }), + new ComponentAttribute({ + name: 'retries', + type: 'Number', + definition: serviceHealthcheckAttributeDef.definedAttributes.find( + ({ name }) => name === 'retries', + ), + value: 3, + }), + ], + }), + new ComponentAttribute({ + name: 'networks', + type: 'Array', + definition: serviceNetworksAttributeDef, + value: ['backend'], + }), + new ComponentAttribute({ ...parentComposeAttribute }), + ], +}); + +const veterinaryMsService = new DockerComposeComponent({ + id: 'veterinary-ms', + path: './veto-full-compose.yaml', + definition: serviceDef, + attributes: [ + new ComponentAttribute({ + name: 'image', + type: 'String', + definition: serviceImageAttributeDef, + value: 'veterinary-ms:0.2', + }), + new ComponentAttribute({ + name: 'build', + type: 'Object', + definition: serviceBuildAttributeDef, + value: [ + new ComponentAttribute({ + name: 'context', + type: 'String', + definition: buildContextAttributeDef, + value: './Backend/veterinary-ms', + }), + ], + }), + new ComponentAttribute({ + name: 'depends_on', + type: 'Array', + definition: dependsOnAttributeDef, + value: [new ComponentAttribute({ + name: null, + type: 'Object', + value: [ + new ComponentAttribute({ + name: 'service_veterinary-ms_0', + type: 'Array', + definition: dependsOnConfigServerDef, + value: ['veterinary-config-server'], + }), + new ComponentAttribute({ + name: 'condition', + type: 'String', + definition: dependsOnConditionDef, + value: 'service_healthy', + }), + ], + }), + new ComponentAttribute({ + name: null, + type: 'Object', + value: [ + new ComponentAttribute({ + name: 'service_veterinary-ms_1', + type: 'Array', + definition: dependsOnDatabaseDef, + value: ['database'], + }), + new ComponentAttribute({ + name: 'condition', + type: 'String', + definition: dependsOnConditionDef, + value: 'service_healthy', + }), + ], + })], + }), + new ComponentAttribute({ + name: 'tty', + type: 'Boolean', + definition: serviceDef.definedAttributes + .find(({ name }) => name === 'tty'), + value: true, + }), + new ComponentAttribute({ ...parentComposeAttribute }), + ], +}); + +const backendNetwork = new DockerComposeComponent({ + id: 'backend', + path: './veto-full-compose.yaml', + definition: networkDef, + attributes: [ + new ComponentAttribute({ + name: 'driver', + type: 'String', + definition: networkDef.definedAttributes + .find(({ name }) => name === 'driver'), + value: 'custom-driver-0', + }), + new ComponentAttribute({ ...parentComposeAttribute }), + ], +}); + +const dataVolume = new DockerComposeComponent({ + id: 'data', + path: './veto-full-compose.yaml', + definition: volumeDef, + attributes: [ + new ComponentAttribute({ + name: 'driver', + type: 'String', + definition: volumeDef.definedAttributes + .find(({ name }) => name === 'driver'), + value: 'custom-driver-1', + }), + new ComponentAttribute({ ...parentComposeAttribute }), + ], +}); + +const secretComponent = new DockerComposeComponent({ + id: 'secret-file', + path: './veto-full-compose.yaml', + definition: secretDef, + attributes: [ + new ComponentAttribute({ + name: 'file', + type: 'String', + definition: secretDef.definedAttributes + .find(({ name }) => name === 'file'), + value: 'path/to/secret/file', + }), + new ComponentAttribute({ ...parentComposeAttribute }), + ], +}); + +const configComponent = new DockerComposeComponent({ + id: 'config-file', + path: './veto-full-compose.yaml', + definition: configDef, + attributes: [ + new ComponentAttribute({ + name: 'file', + type: 'String', + definition: configDef.definedAttributes + .find(({ name }) => name === 'file'), + value: 'path/to/config/file', + }), + new ComponentAttribute({ ...parentComposeAttribute }), + ], +}); + +// Adding components to pluginData +pluginData.components.push(databaseService); +pluginData.components.push(veterinaryConfigServerService); +pluginData.components.push(veterinaryMsService); +pluginData.components.push(backendNetwork); +pluginData.components.push(dataVolume); +pluginData.components.push(secretComponent); +pluginData.components.push(configComponent); +pluginData.components.push(dockerCompose); + +export default pluginData; diff --git a/tests/resources/parser/veto-full-compose.yaml b/tests/resources/parser/veto-full-compose.yaml new file mode 100644 index 0000000..d0a0c45 --- /dev/null +++ b/tests/resources/parser/veto-full-compose.yaml @@ -0,0 +1,50 @@ +version: '3.9' +services: + database: + image: postgres + environment: + - POSTGRES_USER=admin + ports: + - '5432:5432' + networks: + - backend + volumes: + - data:/path/to/mount + secrets: + - secret-file + configs: + - config-file + veterinary-config-server: + image: veterinary-config-server:0.2 + build: + context: ./Backend/config-server + dockerfile: ./Backend/config-server/Dockerfile + healthcheck: + test: curl -f http://localhost:2001/actuator/health + interval: 30s + timeout: 5s + retries: 3 + networks: + - backend + veterinary-ms: + image: veterinary-ms:0.2 + build: + context: ./Backend/veterinary-ms + depends_on: + veterinary-config-server: + condition: service_healthy + database: + condition: service_healthy + tty: true +networks: + backend: + driver: custom-driver-0 +volumes: + data: + driver: custom-driver-1 +secrets: + secret-file: + file: 'path/to/secret/file' +configs: + config-file: + file: 'path/to/config/file' diff --git a/tests/unit/index.spec.js b/tests/unit/index.spec.js index 17237e7..f93885a 100644 --- a/tests/unit/index.spec.js +++ b/tests/unit/index.spec.js @@ -4,4 +4,12 @@ describe('Test index of project', () => { it('should return DockerComposePlugin', () => { expect(new Plugin().constructor.name).toEqual('DockerComposePlugin'); }); + + it('Index should return all needed objects', () => { + expect(Plugin.PluginDrawer).not.toBeNull(); + expect(Plugin.PluginMetadata).not.toBeNull(); + expect(Plugin.PluginParser).not.toBeNull(); + expect(Plugin.PluginRenderer).not.toBeNull(); + expect(Plugin.resources).not.toBeNull(); + }); }); diff --git a/tests/unit/metadata/DockerComposeMetadata.spec.js b/tests/unit/metadata/DockerComposeMetadata.spec.js index e5447be..39bb879 100644 --- a/tests/unit/metadata/DockerComposeMetadata.spec.js +++ b/tests/unit/metadata/DockerComposeMetadata.spec.js @@ -1,21 +1,16 @@ -import DockerComposeMetadata from 'src/metadata/DockerComposeMetadata'; -import { DefaultData } from 'leto-modelizer-plugin-core'; +import { getComposatorMetadata } from 'tests/resources/metadata/getComposatorMetadata'; -describe('Test class: DockerComposeMetadata', () => { - describe('Test method: validate', () => { - it('should return true', () => { - expect(new DockerComposeMetadata().validate()).toEqual(true); - }); - }); - - describe('Test method: parse', () => { - it('should set components definitions to empty array', () => { - const pluginData = new DefaultData(); - pluginData.definitions.components = ['a']; - - new DockerComposeMetadata(pluginData).parse(); - - expect(pluginData.definitions.components).toEqual([]); +describe('Test DockerComposeMetadata', () => { + describe('Test methods', () => { + describe('Test method: validate', () => { + it('Should return true on valid metadata', () => { + const metadata = getComposatorMetadata('validMetadata', 'tests/resources/metadata/valid.json'); + expect(metadata.validate()).toBeTruthy(); + }); + it('Should return false on invalid metadata', () => { + const metadata = getComposatorMetadata('invalidMetadata', 'tests/resources/metadata/invalid.json'); + expect(metadata.validate()).toBeFalsy(); + }); }); }); }); diff --git a/tests/unit/models/DockerComposeComponent.spec.js b/tests/unit/models/DockerComposeComponent.spec.js new file mode 100644 index 0000000..d49af8f --- /dev/null +++ b/tests/unit/models/DockerComposeComponent.spec.js @@ -0,0 +1,82 @@ +import { ComponentAttribute } from 'leto-modelizer-plugin-core'; +import DockerComposeComponent from 'src/models/DockerComposeComponent'; + +describe('Test methods', () => { + describe('Test method: __getAttributeByName', () => { + it('Should find correct attribute when it exists', () => { + const component = new DockerComposeComponent(); + + const objectAttribute = new ComponentAttribute({ + name: 'parentObject', + type: 'Object', + value: [ + new ComponentAttribute({ + name: 'sonObject', + }), + new ComponentAttribute({ + name: 'test-attribute', + }), + ], + }); + + const wrongObjectAttribute = new ComponentAttribute({ + name: 'parentObject', + type: 'Object', + value: [ + new ComponentAttribute({ + name: 'sonObject', + }), + ], + }); + + const arrayAttribute = new ComponentAttribute({ + name: 'parentObject', + type: 'Array', + value: [ + new ComponentAttribute({ + name: 'sonObject', + }), + new ComponentAttribute({ + name: 'test-attribute', + }), + ], + }); + + const wrongAttribute = new ComponentAttribute({ + name: 'wrongName', + }); + + let attributes = [objectAttribute]; + expect(component.__getAttributeByName(attributes, 'test-attribute')?.name).toBe('test-attribute'); + attributes = [arrayAttribute]; + expect(component.__getAttributeByName(attributes, 'test-attribute')?.name).toBe('test-attribute'); + attributes = [wrongAttribute]; + expect(component.__getAttributeByName(attributes, 'test-attribute')).toBe(null); + attributes = [wrongObjectAttribute]; + expect(component.__getAttributeByName(attributes, 'test-attribute')).toBe(null); + }); + + it('Should return null when correct attribute does not exist', () => { + const component = new DockerComposeComponent(); + + const wrongObjectAttribute = new ComponentAttribute({ + name: 'parentObject', + type: 'Object', + value: [ + new ComponentAttribute({ + name: 'sonObject', + }), + ], + }); + + const wrongAttribute = new ComponentAttribute({ + name: 'wrongName', + }); + + let attributes = [wrongAttribute]; + expect(component.__getAttributeByName(attributes, 'test-attribute')).toBe(null); + attributes = [wrongObjectAttribute]; + expect(component.__getAttributeByName(attributes, 'test-attribute')).toBe(null); + }); + }); +}); diff --git a/tests/unit/models/DockerComposeData.spec.js b/tests/unit/models/DockerComposeData.spec.js new file mode 100644 index 0000000..392eaad --- /dev/null +++ b/tests/unit/models/DockerComposeData.spec.js @@ -0,0 +1,17 @@ +import dataGetLinkTestsPluginData from 'tests/resources/models/DataGetLinkTests'; + +describe('DockerComposeData', () => { + describe('Test function: getLinks', () => { + it('should return component links based on service link attributes', () => { + const data = dataGetLinkTestsPluginData; + + const links = data.getLinks(); + expect(links.length).toBe(3); + expect(links).toContainEqual( + expect.objectContaining({ source: 'veterinary-ms', target: 'veterinary-config-server' }), + expect.objectContaining({ source: 'veterinary-ms', target: 'backend' }), + expect.objectContaining({ source: 'veterinary-ms', target: 'data' }), + ); + }); + }); +}); diff --git a/tests/unit/models/DockerComposePlugin.spec.js b/tests/unit/models/DockerComposePlugin.spec.js index 501984b..949bdcd 100644 --- a/tests/unit/models/DockerComposePlugin.spec.js +++ b/tests/unit/models/DockerComposePlugin.spec.js @@ -1,15 +1,9 @@ import DockerComposePlugin from 'src/models/DockerComposePlugin'; -describe('Test class: DockerComposePlugin', () => { - describe('Test constructor', () => { - it('Check variable initialization', () => { - const plugin = new DockerComposePlugin(); - - expect(plugin.data).not.toBeNull(); - expect(plugin.__drawer).not.toBeNull(); - expect(plugin.__parser).not.toBeNull(); - expect(plugin.__metadata).not.toBeNull(); - expect(plugin.__renderer).not.toBeNull(); - }); +describe('DockerComposePlugin', () => { + it('should create a DockerComposePlugin instance ', () => { + const plugin = new DockerComposePlugin(); + expect(plugin).not.toBeNull(); + expect(plugin).toBeInstanceOf(DockerComposePlugin); }); }); diff --git a/tests/unit/parser/DockerComposeListener.spec.js b/tests/unit/parser/DockerComposeListener.spec.js new file mode 100644 index 0000000..604c651 --- /dev/null +++ b/tests/unit/parser/DockerComposeListener.spec.js @@ -0,0 +1,67 @@ +import DockerComposeListener from 'src/parser/DockerComposeListener'; + +describe('Test DockerComposeListener', () => { + describe('Test functions', () => { + describe('Test function: lidyToLetoType', () => { + it('Should convert type correctly', () => { + const listener = new DockerComposeListener(); + expect(listener.lidyToLetoType('string')).toBe('String'); + expect(listener.lidyToLetoType('boolean')).toBe('Boolean'); + expect(listener.lidyToLetoType('int')).toBe('Number'); + expect(listener.lidyToLetoType('float')).toBe('Number'); + expect(listener.lidyToLetoType('map')).toBe('Object'); + expect(listener.lidyToLetoType('list')).toBe('Array'); + expect(listener.lidyToLetoType('something else')).toBe(null); + }); + }); + + describe('Test function: exit_root', () => { + it('Should do nothing if rootNode.value.version is not defined', () => { + const listener = new DockerComposeListener(); + expect(listener.exit_root({ value: 'value does not have version' })).not.toBeDefined(); + }); + }); + + describe('Test function: exit_service', () => { + it('Should do nothing if serviceNode is not defined', () => { + const listener = new DockerComposeListener(); + expect(listener.exit_service(undefined)).not.toBeDefined(); + }); + }); + + describe('Test function: exit_volume', () => { + it('Should do nothing if columeNode is not defined', () => { + const listener = new DockerComposeListener(); + expect(listener.exit_volume(undefined)).not.toBeDefined(); + }); + }); + + describe('Test function: exit_network', () => { + it('Should do nothing if networkNode is not defined', () => { + const listener = new DockerComposeListener(); + expect(listener.exit_network(undefined)).not.toBeDefined(); + }); + }); + + describe('Test function: exit_config', () => { + it('Should do nothing if configNode is not defined', () => { + const listener = new DockerComposeListener(); + expect(listener.exit_config(undefined)).not.toBeDefined(); + }); + }); + + describe('Test function: exit_secret', () => { + it('Should do nothing if secretNode is not defined', () => { + const listener = new DockerComposeListener(); + expect(listener.exit_secret(undefined)).not.toBeDefined(); + }); + }); + + describe('Test function: createComponentFromTree', () => { + it('Should return null if type is not in list', () => { + const listener = new DockerComposeListener(); + expect(listener.createComponentFromTree({}, 'NonExistentType')).toBeNull(); + }); + }); + }); +}); diff --git a/tests/unit/parser/DockerComposeParser.spec.js b/tests/unit/parser/DockerComposeParser.spec.js index 9dded44..4d61f3c 100644 --- a/tests/unit/parser/DockerComposeParser.spec.js +++ b/tests/unit/parser/DockerComposeParser.spec.js @@ -1,36 +1,94 @@ +import fs from 'fs'; +import { FileInformation, FileInput } from 'leto-modelizer-plugin-core'; import DockerComposeParser from 'src/parser/DockerComposeParser'; -import { - DefaultData, - FileInformation, -} from 'leto-modelizer-plugin-core'; +import DockerComposeMetadata from 'src/metadata/DockerComposeMetadata'; +import DockerComposeData from 'src/models/DockerComposeData'; + +import mockData from 'tests/resources/parser/veto-full-compose'; +import emptyComposeMockData from 'tests/resources/parser/empty-compose'; describe('Test DockerComposeParser', () => { - describe('Test methods', () => { - describe('Test method: isParsable', () => { - it('should return false', () => { - expect(new DockerComposeParser().isParsable(new FileInformation({ - path: '', - }))).toEqual(false); + describe('Test functions', () => { + describe('Test function: isParsable', () => { + it('Should return true on .yml file', () => { + const parser = new DockerComposeParser(); + const file = new FileInformation({ path: 'simple.yml' }); + + expect(parser.isParsable(file)).toEqual(true); }); - }); - describe('Test method: getModels', () => { - it('should return an empty array without parameter', () => { + it('Should return true on .yaml file', () => { + const parser = new DockerComposeParser(); + const file = new FileInformation({ path: 'simple.yaml' }); + + expect(parser.isParsable(file)).toEqual(true); + }); + + it('Should return false on file that is not a YAML file', () => { const parser = new DockerComposeParser(); + const file = new FileInformation({ path: 'file.txt' }); - expect(parser.getModels()).toEqual([]); + expect(parser.isParsable(file)).toEqual(false); + }); + + it('Should return false on wrong file', () => { + const parser = new DockerComposeParser(); + const file = new FileInformation({ path: '.github/workflows/simple.tf' }); + + expect(parser.isParsable(file)).toEqual(false); }); }); - describe('Test method: parse', () => { - it('should set pluginData components and parseErrors to empty array', () => { - const pluginData = new DefaultData(); + describe('Test function: parse', () => { + it('Should set empty components on no input files', () => { + const pluginData = new DockerComposeData(); const parser = new DockerComposeParser(pluginData); - parser.parse(); - expect(parser.pluginData.components).toEqual([]); - expect(parser.pluginData.parseErrors).toEqual([]); + expect(pluginData.components).not.toBeNull(); + expect(pluginData.components.length).toEqual(0); + }); + + it('Should set empty components on null input files', () => { + const pluginData = new DockerComposeData(); + const parser = new DockerComposeParser(pluginData); + const file = new FileInput({ + path: '', + content: null, + }); + parser.parse(new FileInformation({ path: '' }), [file]); + + expect(pluginData.components).not.toBeNull(); + expect(pluginData.components.length).toEqual(0); + }); + + it('Should set valid components', () => { + const pluginData = new DockerComposeData(); + const metadata = new DockerComposeMetadata(pluginData); + metadata.parse(); + + const parser = new DockerComposeParser(pluginData); + const file = new FileInput({ + path: './veto-full-compose.yaml', + content: fs.readFileSync('tests/resources/parser/veto-full-compose.yaml', 'utf8'), + }); + parser.parse(new FileInformation({ path: './veto-full-compose.yaml' }), [file]); + expect(pluginData.components).toEqual(mockData.components); + }); + + it('Should set empty children on file containing only docker-compose element', () => { + const pluginData = new DockerComposeData(); + const metadata = new DockerComposeMetadata(pluginData); + metadata.parse(); + + const parser = new DockerComposeParser(pluginData); + + const file = new FileInput({ + path: './empty-compose.yaml', + content: fs.readFileSync('tests/resources/parser/empty-compose.yaml', 'utf8'), + }); + parser.parse(new FileInformation({ path: './empty-compose.yaml' }), [file]); + expect(pluginData.components).toEqual(emptyComposeMockData.components); }); }); });