Merge pull request #6112 from strapi/chore/configuration-dump-cli

Configuration dump and restore cli
This commit is contained in:
Alexandre BODIN 2020-05-13 16:51:26 +02:00 committed by GitHub
commit 2f84e42c44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 306 additions and 7 deletions

View File

@ -78,6 +78,68 @@ options: [--no-optimization]
- **strapi build --no-optimization**<br/>
Builds the administration panel without minimizing the assets. The build duration is faster.
## strapi configuration:dump|config:dump
Dumps configurations to a file or stdout to help you migrate to production.
The dump format will be a JSON array.
```
strapi configuration:dump
Options:
-f, --file <file> Output file, default output is stdout
```
**Examples**
- `strapi configuration:dump -f dump.json`
- `strapi config:dump --file dump.json`
- `strapi config:dump > dump.json`
All these examples are equivalent.
::: warning
When configuring your application you often enter credentials for thrid party services (e.g authentication providers). Be aware that those credentials will also be dumped into the output of this command.
In case of doubt, you should avoid committing the dump file into a versioning system. Here are some methods you can explore:
- Copy the file directly to the environment you want and run the restore command there.
- Put the file in a secure location and download it at deploy time with the right credentials.
- Encrypt the file before committing and decrypt it when running the restore command.
:::
## strapi configuration:restore|config:restore
Restores a configuration dump into your application.
The input format must be a JSON array.
```
strapi configuration:restore
Options:
-f, --file <file> Input file, default input is stdin
-s, --strategy <strategy> Strategy name, one of: "replace", "merge", "keep". Defaults to: "replace"
```
**Examples**
- `strapi configuration:restore -f dump.json`
- `strapi config:restore --file dump.json -s replace`
- `cat dump.json | strapi config:restore`
- `strapi config:restore < dump.json`
All these examples are equivalent.
**Strategies**
When running the restore command, you can choose from three different strategies:
- **replace**: Will create missing keys and replace existing ones.
- **merge**: Will create missing keys and merge existing keys whith there new value.
- **keep**: Will create missing keys and keep existing keys as is.
## strapi generate:api
Scaffold a complete API with its configurations, controller, model and service.

View File

@ -49,7 +49,16 @@ const getLocalScript = name => (...args) => {
process.exit(1);
}
return require(cmdPath)(...args);
const script = require(cmdPath);
Promise.resolve()
.then(() => {
return script(...args);
})
.catch(error => {
console.error(`Error while running command ${name}: ${error.message}`);
process.exit(1);
});
};
/**
@ -205,6 +214,19 @@ program
.description('Starts the admin dev server')
.action(getLocalScript('watchAdmin'));
program
.command('configuration:dump')
.alias('config:dump')
.option('-f, --file <file>', 'Output file, default output is stdout')
.action(getLocalScript('configurationDump'));
program
.command('configuration:restore')
.alias('config:restore')
.option('-f, --file <file>', 'Input file, default input is stdin')
.option('-s, --strategy <strategy>', 'Strategy name, one of: "replace", "merge", "keep"')
.action(getLocalScript('configurationRestore'));
/**
* Normalize help argument
*/

View File

@ -53,6 +53,7 @@ class Strapi {
this.admin = {};
this.plugins = {};
this.config = loadConfiguration(this.dir, opts);
this.isLoaded = false;
// internal services.
this.fs = createStrapiFs(this);
@ -157,12 +158,9 @@ class Strapi {
async start(cb) {
try {
await this.load();
// Run bootstrap function.
await this.runBootstrapFunctions();
// Freeze object.
await this.freeze();
if (!this.isLoaded) {
await this.load();
}
this.app.use(this.router.routes()).use(this.router.allowedMethods());
@ -324,6 +322,13 @@ class Strapi {
// Initialize hooks and middlewares.
await initializeMiddlewares.call(this);
await initializeHooks.call(this);
await this.runBootstrapFunctions();
await this.freeze();
this.isLoaded = true;
return this;
}
async startWebhooks() {

View File

@ -0,0 +1,50 @@
'use strict';
const fs = require('fs');
const strapi = require('../index');
const CHUNK_SIZE = 100;
/**
* Will dump configurations to a file or stdout
* @param {string} file filepath to use as output
*/
module.exports = async function({ file: filePath }) {
const output = filePath ? fs.createWriteStream(filePath) : process.stdout;
const app = await strapi().load();
const count = await app.query('core_store').count();
const exportData = [];
const pageCount = Math.ceil(count / CHUNK_SIZE);
for (let page = 0; page < pageCount; page++) {
const results = await app
.query('core_store')
.find({ _limit: CHUNK_SIZE, _start: page * CHUNK_SIZE, _sort: 'key' });
results
.filter(result => result.key.startsWith('plugin_'))
.forEach(result => {
exportData.push({
key: result.key,
value: result.value,
type: result.type,
environment: result.environment,
tag: result.tag,
});
});
}
output.write(JSON.stringify(exportData));
output.write('\n');
output.end();
// log success only when writting to file
if (filePath) {
console.log(`Successfully exported ${exportData.length} configuration entries`);
}
process.exit(0);
};

View File

@ -0,0 +1,160 @@
'use strict';
const _ = require('lodash');
const fs = require('fs');
const strapi = require('../index');
/**
* Will restore configurations. It reads from a file or stdin
* @param {string} file filepath to use as input
* @param {string} strategy import strategy. one of (replace, merge, keep, default: replace)
*/
module.exports = async function({ file: filePath, strategy = 'replace' }) {
const input = filePath ? fs.readFileSync(filePath) : await readStdin(process.stdin);
const app = await strapi().load();
let dataToImport;
try {
dataToImport = JSON.parse(input);
} catch (error) {
throw new Error(`Invalid input data: ${error.message}. Expected a valid JSON array.`);
}
if (!Array.isArray(dataToImport)) {
throw new Error(`Invalid input data. Expected a valid JSON array.`);
}
const importer = createImporter(app.db, strategy);
for (const config of dataToImport) {
await importer.import(config);
}
console.log(
`Successfully imported configuration with ${strategy} strategy. Statistics: ${importer.printStatistics()}.`
);
process.exit(0);
};
const readStdin = () => {
const { stdin } = process;
let result = '';
if (stdin.isTTY) return Promise.resolve(result);
return new Promise((resolve, reject) => {
stdin.setEncoding('utf8');
stdin.on('readable', () => {
let chunk;
while ((chunk = stdin.read())) {
result += chunk;
}
});
stdin.on('end', () => {
resolve(result);
});
stdin.on('error', reject);
});
};
const createImporter = (db, strategy) => {
switch (strategy) {
case 'replace':
return createReplaceImporter(db);
case 'merge':
return createMergeImporter(db);
case 'keep':
return createKeepImporter(db);
default:
throw new Error(`No importer available for strategy "${strategy}"`);
}
};
/**
* Replace importer. Will replace the keys that already exist and create the new ones
* @param {Object} db - DatabaseManager instance
*/
const createReplaceImporter = db => {
const stats = {
created: 0,
replaced: 0,
};
return {
printStatistics() {
return `${stats.created} created, ${stats.replaced} replaced`;
},
async import(conf) {
const matching = await db.query('core_store').count({ key: conf.key });
if (matching > 0) {
stats.replaced += 1;
await db.query('core_store').update({ key: conf.key }, conf);
} else {
stats.created += 1;
await db.query('core_store').create(conf);
}
},
};
};
/**
* Merge importer. Will merge the keys that already exist with their new value and create the new ones
* @param {Object} db - DatabaseManager instance
*/
const createMergeImporter = db => {
const stats = {
created: 0,
merged: 0,
};
return {
printStatistics() {
return `${stats.created} created, ${stats.merged} merged`;
},
async import(conf) {
const existingConf = await db.query('core_store').find({ key: conf.key });
if (existingConf) {
stats.merged += 1;
await db.query('core_store').update({ key: conf.key }, _.merge(existingConf, conf));
} else {
stats.created += 1;
await db.query('core_store').create(conf);
}
},
};
};
/**
* Merge importer. Will keep the keys that already exist without changing them and create the new ones
* @param {Object} db - DatabaseManager instance
*/
const createKeepImporter = db => {
const stats = {
created: 0,
untouched: 0,
};
return {
printStatistics() {
return `${stats.created} created, ${stats.untouched} untouched`;
},
async import(conf) {
const matching = await db.query('core_store').count({ key: conf.key });
if (matching > 0) {
stats.untouched += 1;
// if configuration already exists do not overwrite it
return;
}
stats.created += 1;
await db.query('core_store').create(conf);
},
};
};