@tmlmobilidade/env-sync 20260304.1625.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -0
- package/dist/artifacts/upload.service.d.ts +17 -0
- package/dist/artifacts/upload.service.js +181 -0
- package/dist/cli/commands.d.ts +12 -0
- package/dist/cli/commands.js +111 -0
- package/dist/config/config-loader.d.ts +37 -0
- package/dist/config/config-loader.js +91 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +176 -0
- package/dist/mongodb/dump.service.d.ts +8 -0
- package/dist/mongodb/dump.service.js +43 -0
- package/dist/mongodb/restore.service.d.ts +6 -0
- package/dist/mongodb/restore.service.js +28 -0
- package/dist/mongodb/sync.service.d.ts +7 -0
- package/dist/mongodb/sync.service.js +106 -0
- package/dist/storage/rclone.service.d.ts +2 -0
- package/dist/storage/rclone.service.js +78 -0
- package/dist/storage/sync.service.d.ts +2 -0
- package/dist/storage/sync.service.js +11 -0
- package/dist/utils/exec.d.ts +12 -0
- package/dist/utils/exec.js +114 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +37 -0
- package/dist/utils/metadata.d.ts +6 -0
- package/dist/utils/metadata.js +56 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Environment Sync CLI
|
|
2
|
+
|
|
3
|
+
CLI tool to sync production and staging environments for MongoDB and Storage (using RClone).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run build
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
### Basic Usage
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Sync both MongoDB and Storage (interactive mode)
|
|
18
|
+
npm run dev
|
|
19
|
+
|
|
20
|
+
# Run with arguments (use -- to pass arguments to the script)
|
|
21
|
+
npm run dev -- --help
|
|
22
|
+
npm run dev -- --db-only
|
|
23
|
+
npm run dev -- --storage-only
|
|
24
|
+
|
|
25
|
+
# Or after building:
|
|
26
|
+
./dist/index.js
|
|
27
|
+
./dist/index.js --help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Command Line Options
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Sync only MongoDB database
|
|
34
|
+
env-sync --db-only
|
|
35
|
+
|
|
36
|
+
# Sync only storage
|
|
37
|
+
env-sync --storage-only
|
|
38
|
+
|
|
39
|
+
# Use replica set mode
|
|
40
|
+
env-sync --replica-set
|
|
41
|
+
|
|
42
|
+
# Skip cleanup of old backups
|
|
43
|
+
env-sync --no-cleanup
|
|
44
|
+
|
|
45
|
+
# Upload backup artifacts to OCI bucket (for CI/CD, replaces GitHub artifacts)
|
|
46
|
+
env-sync --db-only --upload-artifacts
|
|
47
|
+
|
|
48
|
+
# Show help
|
|
49
|
+
env-sync --help
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
Create a `.env` file in the `cli/env-sync-ts/` directory with the following variables:
|
|
55
|
+
|
|
56
|
+
### MongoDB Configuration
|
|
57
|
+
|
|
58
|
+
```env
|
|
59
|
+
# Production MongoDB
|
|
60
|
+
PROD_HOST=production-mongo-host:27017
|
|
61
|
+
PROD_USERNAME=admin
|
|
62
|
+
PROD_PASSWORD=password
|
|
63
|
+
PROD_AUTH_DATABASE=admin
|
|
64
|
+
PROD_DB=production_database
|
|
65
|
+
|
|
66
|
+
# Staging MongoDB
|
|
67
|
+
STAGING_HOST=staging-mongo-host:27017
|
|
68
|
+
STAGING_USERNAME=admin
|
|
69
|
+
STAGING_PASSWORD=password
|
|
70
|
+
STAGING_AUTH_DATABASE=admin
|
|
71
|
+
STAGING_DB=staging_database
|
|
72
|
+
|
|
73
|
+
# Optional: Collections to exclude from sync (space-separated)
|
|
74
|
+
EXCLUDE_COLLECTIONS=logs sessions temp_data
|
|
75
|
+
|
|
76
|
+
# Optional: Backup retention days (default: 7)
|
|
77
|
+
BACKUP_RETENTION_DAYS=7
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Storage Configuration (OCI/RClone)
|
|
81
|
+
|
|
82
|
+
```env
|
|
83
|
+
# RClone Configuration
|
|
84
|
+
STORAGE_REMOTE_NAME=oci_storage
|
|
85
|
+
STORAGE_TYPE=oracleobjectstorage
|
|
86
|
+
STORAGE_SOURCE=production-bucket/path/to/source
|
|
87
|
+
STORAGE_DEST=staging-bucket/path/to/dest
|
|
88
|
+
|
|
89
|
+
# OCI Authentication
|
|
90
|
+
OCI_USER=ocid1.user.oc1..
|
|
91
|
+
OCI_FINGERPRINT=aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99
|
|
92
|
+
OCI_KEY_FILE=/path/to/private_key.pem
|
|
93
|
+
OCI_TENANCY=ocid1.tenancy.oc1..
|
|
94
|
+
OCI_REGION=us-ashburn-1
|
|
95
|
+
OCI_COMPARTMENT=ocid1.compartment.oc1..
|
|
96
|
+
OCI_NAMESPACE=your_namespace
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Artifacts Configuration (for CI/CD)
|
|
100
|
+
|
|
101
|
+
```env
|
|
102
|
+
# OCI bucket for storing backup artifacts (required for --upload-artifacts)
|
|
103
|
+
ARTIFACTS_BUCKET=your-artifacts-bucket
|
|
104
|
+
|
|
105
|
+
# Optional: prefix/folder within the bucket (default: "env-sync")
|
|
106
|
+
ARTIFACTS_PREFIX=env-sync
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## MongoDB Backup Strategy
|
|
110
|
+
|
|
111
|
+
- Full database dump with all collections (excluding configured collections)
|
|
112
|
+
- Dumps all collections from production database
|
|
113
|
+
- Restores to staging database with `--drop` flag
|
|
114
|
+
|
|
115
|
+
### Backup Metadata
|
|
116
|
+
- Backup metadata is stored in `backups/.backup_metadata`
|
|
117
|
+
- Tracks last backup timestamp
|
|
118
|
+
|
|
119
|
+
## Storage Sync
|
|
120
|
+
|
|
121
|
+
- Uses RClone to sync files from production OCI storage to staging
|
|
122
|
+
- Configured via environment variables (no rclone config file needed)
|
|
123
|
+
- Progress reporting during sync
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Run in development mode
|
|
129
|
+
npm run dev
|
|
130
|
+
|
|
131
|
+
# Build
|
|
132
|
+
npm run build
|
|
133
|
+
|
|
134
|
+
# Lint
|
|
135
|
+
npm run lint
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Requirements
|
|
139
|
+
|
|
140
|
+
- Node.js (v18+)
|
|
141
|
+
- MongoDB tools (`mongodump`, `mongorestore`)
|
|
142
|
+
- RClone (`rclone`)
|
|
143
|
+
|
|
144
|
+
## Notes
|
|
145
|
+
|
|
146
|
+
- The script maintains compatibility with the existing bash scripts' `.env` file format
|
|
147
|
+
- Backups are stored in `backups/` directory
|
|
148
|
+
- Old backups are automatically cleaned up (configurable retention period)
|
|
149
|
+
- Interactive prompts use Clack for a modern CLI experience
|
|
150
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StorageConfig } from '../config/config-loader.js';
|
|
2
|
+
export interface UploadArtifactsOptions {
|
|
3
|
+
/** OCI bucket name to upload to */
|
|
4
|
+
bucket: string;
|
|
5
|
+
/** Local directory containing artifacts to upload */
|
|
6
|
+
localPath: string;
|
|
7
|
+
/** Optional prefix/folder within the bucket */
|
|
8
|
+
prefix?: string;
|
|
9
|
+
/** Storage config with OCI credentials */
|
|
10
|
+
storageConfig: StorageConfig;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Upload artifacts (backups) to OCI Object Storage bucket.
|
|
14
|
+
* This replaces the GitHub artifacts upload for security in public repos.
|
|
15
|
+
* The backup folder is zipped before uploading to reduce size and upload time.
|
|
16
|
+
*/
|
|
17
|
+
export declare function uploadArtifacts(options: UploadArtifactsOptions): Promise<void>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import archiver from 'archiver';
|
|
2
|
+
import { createWriteStream, existsSync, readdirSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { checkCommandAvailable, execCommandStream } from '../utils/exec.js';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
/**
|
|
8
|
+
* Build a temporary OCI CLI config file using the credentials from StorageConfig.
|
|
9
|
+
* This lets rclone use the "user_principal_auth" provider without needing ~/.oci/config.
|
|
10
|
+
*/
|
|
11
|
+
function buildOciConfig(config, profileName) {
|
|
12
|
+
return [
|
|
13
|
+
`[${profileName}]`,
|
|
14
|
+
`user=${config.user}`,
|
|
15
|
+
`fingerprint=${config.fingerprint}`,
|
|
16
|
+
`key_file=${config.keyFile}`,
|
|
17
|
+
`tenancy=${config.tenancy}`,
|
|
18
|
+
`region=${config.region}`,
|
|
19
|
+
'',
|
|
20
|
+
].join('\n');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build the rclone backend config pointing at the temporary OCI config file.
|
|
24
|
+
*/
|
|
25
|
+
function buildRcloneConfig(config, ociConfigPath, profileName) {
|
|
26
|
+
return [
|
|
27
|
+
`[${config.remoteName}]`,
|
|
28
|
+
`type = ${config.type}`,
|
|
29
|
+
`namespace = ${config.namespace}`,
|
|
30
|
+
`compartment = ${config.compartment}`,
|
|
31
|
+
`region = ${config.region}`,
|
|
32
|
+
'provider = user_principal_auth',
|
|
33
|
+
`config_file = ${ociConfigPath}`,
|
|
34
|
+
`config_profile = ${profileName}`,
|
|
35
|
+
'',
|
|
36
|
+
].join('\n');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if the directory is empty or has no files to upload
|
|
40
|
+
*/
|
|
41
|
+
function isDirectoryEmpty(dirPath) {
|
|
42
|
+
if (!existsSync(dirPath)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const stats = statSync(dirPath);
|
|
46
|
+
if (!stats.isDirectory()) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const entries = readdirSync(dirPath);
|
|
50
|
+
// Filter out hidden files like .backup_metadata
|
|
51
|
+
const files = entries.filter((entry) => {
|
|
52
|
+
const entryPath = path.join(dirPath, entry);
|
|
53
|
+
const entryStats = statSync(entryPath);
|
|
54
|
+
return entryStats.isFile();
|
|
55
|
+
});
|
|
56
|
+
return files.length === 0;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create a zip archive of the backup directory using archiver
|
|
60
|
+
*/
|
|
61
|
+
async function createZipArchive(sourceDir, outputPath) {
|
|
62
|
+
logger.info(`Creating zip archive: ${outputPath}`);
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const output = createWriteStream(outputPath);
|
|
65
|
+
const archive = archiver('zip', {
|
|
66
|
+
zlib: { level: 9 }, // Maximum compression
|
|
67
|
+
});
|
|
68
|
+
// Listen for all archive data to be written
|
|
69
|
+
output.on('close', () => {
|
|
70
|
+
logger.verbose(`Zip archive created successfully: ${outputPath} (${archive.pointer()} bytes)`);
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
// Catch warnings (e.g., stat failures and other non-blocking errors)
|
|
74
|
+
archive.on('warning', (err) => {
|
|
75
|
+
if (err.code === 'ENOENT') {
|
|
76
|
+
logger.verbose(`Archive warning: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
reject(err);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// Catch errors
|
|
83
|
+
archive.on('error', (err) => {
|
|
84
|
+
reject(new Error(`Failed to create zip archive: ${err.message}`));
|
|
85
|
+
});
|
|
86
|
+
// Pipe archive data to the file
|
|
87
|
+
archive.pipe(output);
|
|
88
|
+
// Append the entire directory to the archive
|
|
89
|
+
const folderName = path.basename(sourceDir);
|
|
90
|
+
archive.directory(sourceDir, folderName);
|
|
91
|
+
// Finalize the archive (i.e., we are done appending files)
|
|
92
|
+
archive.finalize();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Upload artifacts (backups) to OCI Object Storage bucket.
|
|
97
|
+
* This replaces the GitHub artifacts upload for security in public repos.
|
|
98
|
+
* The backup folder is zipped before uploading to reduce size and upload time.
|
|
99
|
+
*/
|
|
100
|
+
export async function uploadArtifacts(options) {
|
|
101
|
+
const { bucket, localPath, prefix, storageConfig } = options;
|
|
102
|
+
logger.info('Starting artifact upload to OCI Object Storage...');
|
|
103
|
+
// Check if local path exists
|
|
104
|
+
if (!existsSync(localPath)) {
|
|
105
|
+
logger.warn(`Artifact path does not exist: ${localPath}`);
|
|
106
|
+
logger.info('No artifacts to upload.');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Check if directory is empty
|
|
110
|
+
if (isDirectoryEmpty(localPath)) {
|
|
111
|
+
logger.warn(`Artifact directory is empty: ${localPath}`);
|
|
112
|
+
logger.info('No artifacts to upload.');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Check if rclone is available
|
|
116
|
+
if (!(await checkCommandAvailable('rclone'))) {
|
|
117
|
+
throw new Error('rclone not found. Please install rclone.');
|
|
118
|
+
}
|
|
119
|
+
// Create zip archive
|
|
120
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
121
|
+
const zipFileName = `backups-${timestamp}.zip`;
|
|
122
|
+
const zipFilePath = path.join(os.tmpdir(), zipFileName);
|
|
123
|
+
let zipCreated = false;
|
|
124
|
+
try {
|
|
125
|
+
await createZipArchive(localPath, zipFilePath);
|
|
126
|
+
zipCreated = true;
|
|
127
|
+
if (existsSync(zipFilePath)) {
|
|
128
|
+
const zipSizeMB = Math.round(statSync(zipFilePath).size / 1024 / 1024 * 100) / 100;
|
|
129
|
+
logger.info(`Zip archive created: ${zipFileName} (${zipSizeMB} MB)`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
logger.info(`Zip archive created: ${zipFileName}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
logger.error(`Failed to create zip archive: ${error instanceof Error ? error.message : String(error)}`);
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
// Prepare temporary OCI config
|
|
140
|
+
logger.info('Building temporary OCI config...');
|
|
141
|
+
const profileName = 'Default';
|
|
142
|
+
const ociConfigFile = path.join(os.tmpdir(), `oci-artifacts-${Date.now()}.conf`);
|
|
143
|
+
const ociConfigContent = buildOciConfig(storageConfig, profileName);
|
|
144
|
+
writeFileSync(ociConfigFile, ociConfigContent);
|
|
145
|
+
// Build rclone config pointing at the temporary OCI config
|
|
146
|
+
logger.info('Building rclone config...');
|
|
147
|
+
const rcloneConfig = buildRcloneConfig(storageConfig, ociConfigFile, profileName);
|
|
148
|
+
const rcloneConfigFile = path.join(os.tmpdir(), `rclone-artifacts-${Date.now()}.conf`);
|
|
149
|
+
writeFileSync(rcloneConfigFile, rcloneConfig);
|
|
150
|
+
// Build the destination path
|
|
151
|
+
const destFolder = prefix ?? '';
|
|
152
|
+
const destPath = `${storageConfig.remoteName}:${bucket}/${destFolder}`;
|
|
153
|
+
logger.info(`Uploading zip archive to: ${destPath}`);
|
|
154
|
+
// Use rclone copy to upload the zip file
|
|
155
|
+
const baseCmd = `rclone copy "${zipFilePath}" "${destPath}/" --config ${rcloneConfigFile}`;
|
|
156
|
+
const verboseFlags = logger.isVerbose() ? ' --progress --verbose' : '';
|
|
157
|
+
const copyCmd = `${baseCmd}${verboseFlags}`;
|
|
158
|
+
try {
|
|
159
|
+
await execCommandStream(copyCmd);
|
|
160
|
+
logger.success(`Artifacts uploaded successfully to: ${destPath}/${zipFileName}`);
|
|
161
|
+
// Clean up temporary files
|
|
162
|
+
rmSync(ociConfigFile);
|
|
163
|
+
rmSync(rcloneConfigFile);
|
|
164
|
+
if (zipCreated && existsSync(zipFilePath)) {
|
|
165
|
+
rmSync(zipFilePath);
|
|
166
|
+
logger.verbose('Temporary zip file cleaned up');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
// Clean up on error too
|
|
171
|
+
if (existsSync(ociConfigFile))
|
|
172
|
+
rmSync(ociConfigFile);
|
|
173
|
+
if (existsSync(rcloneConfigFile))
|
|
174
|
+
rmSync(rcloneConfigFile);
|
|
175
|
+
if (zipCreated && existsSync(zipFilePath)) {
|
|
176
|
+
rmSync(zipFilePath);
|
|
177
|
+
}
|
|
178
|
+
logger.error(`Artifact upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CliOptions {
|
|
2
|
+
backupOnly?: boolean;
|
|
3
|
+
dbOnly?: boolean;
|
|
4
|
+
help?: boolean;
|
|
5
|
+
noCleanup?: boolean;
|
|
6
|
+
replicaSet?: boolean;
|
|
7
|
+
storageOnly?: boolean;
|
|
8
|
+
uploadArtifacts?: boolean;
|
|
9
|
+
verbose?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseArgs(args: string[]): CliOptions;
|
|
12
|
+
export declare function showHelp(): void;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export function parseArgs(args) {
|
|
2
|
+
const options = {};
|
|
3
|
+
for (const arg of args) {
|
|
4
|
+
switch (arg) {
|
|
5
|
+
case '--backup-only':
|
|
6
|
+
options.backupOnly = true;
|
|
7
|
+
break;
|
|
8
|
+
case '--db-only':
|
|
9
|
+
options.dbOnly = true;
|
|
10
|
+
break;
|
|
11
|
+
case '--help':
|
|
12
|
+
case '-h':
|
|
13
|
+
options.help = true;
|
|
14
|
+
break;
|
|
15
|
+
case '--no-cleanup':
|
|
16
|
+
options.noCleanup = true;
|
|
17
|
+
break;
|
|
18
|
+
case '--no-replica-set':
|
|
19
|
+
options.replicaSet = false;
|
|
20
|
+
break;
|
|
21
|
+
case '--replica-set':
|
|
22
|
+
options.replicaSet = true;
|
|
23
|
+
break;
|
|
24
|
+
case '--storage-only':
|
|
25
|
+
options.storageOnly = true;
|
|
26
|
+
break;
|
|
27
|
+
case '--upload-artifacts':
|
|
28
|
+
options.uploadArtifacts = true;
|
|
29
|
+
break;
|
|
30
|
+
case '--verbose':
|
|
31
|
+
case '-v':
|
|
32
|
+
options.verbose = true;
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
if (arg.startsWith('-')) {
|
|
36
|
+
throw new Error(`Unknown option: ${arg}\nRun 'env-sync --help' for more information.`);
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return options;
|
|
42
|
+
}
|
|
43
|
+
export function showHelp() {
|
|
44
|
+
console.log(`
|
|
45
|
+
Environment Sync CLI - Production to Staging
|
|
46
|
+
|
|
47
|
+
SYNOPSIS
|
|
48
|
+
env-sync [OPTIONS]
|
|
49
|
+
|
|
50
|
+
DESCRIPTION
|
|
51
|
+
Syncs production environment data to staging, handling both OCI file storage
|
|
52
|
+
and MongoDB database. Performs full MongoDB database dumps and restores.
|
|
53
|
+
|
|
54
|
+
MongoDB Backup Strategy:
|
|
55
|
+
- Full database dump with all collections (excluding configured collections)
|
|
56
|
+
- Restores to staging database with --drop flag
|
|
57
|
+
|
|
58
|
+
OPTIONS
|
|
59
|
+
--backup-only Backup MongoDB only (dump without restoring to staging)
|
|
60
|
+
--db-only Sync only MongoDB database, skip file sync
|
|
61
|
+
--storage-only Sync only OCI files, skip database sync
|
|
62
|
+
--upload-artifacts Upload backup artifacts to OCI bucket (instead of GitHub artifacts)
|
|
63
|
+
--replica-set Use replica set sync mode (overrides .env setting)
|
|
64
|
+
--no-replica-set Disable replica set sync mode (overrides .env setting)
|
|
65
|
+
--no-cleanup Skip cleanup of old backups (older than 7 days)
|
|
66
|
+
-v, --verbose Enable verbose output (show detailed command execution)
|
|
67
|
+
-h, --help Show this help message and exit
|
|
68
|
+
|
|
69
|
+
EXAMPLES
|
|
70
|
+
# Sync both files and database (default behavior)
|
|
71
|
+
env-sync
|
|
72
|
+
|
|
73
|
+
# Sync only database
|
|
74
|
+
env-sync --db-only
|
|
75
|
+
|
|
76
|
+
# Sync files only
|
|
77
|
+
env-sync --storage-only
|
|
78
|
+
|
|
79
|
+
# Sync with replica set mode
|
|
80
|
+
env-sync --replica-set
|
|
81
|
+
|
|
82
|
+
# Upload backup artifacts to OCI bucket (for CI/CD)
|
|
83
|
+
env-sync --upload-artifacts
|
|
84
|
+
|
|
85
|
+
# Backup database only (no restore to staging, no storage sync)
|
|
86
|
+
env-sync --backup-only
|
|
87
|
+
|
|
88
|
+
# Backup and upload artifacts to OCI bucket
|
|
89
|
+
env-sync --backup-only --upload-artifacts
|
|
90
|
+
|
|
91
|
+
# Or combine with sync operations
|
|
92
|
+
env-sync --db-only --upload-artifacts
|
|
93
|
+
|
|
94
|
+
CONFIGURATION
|
|
95
|
+
The script reads configuration from .env file in the script directory.
|
|
96
|
+
Required variables:
|
|
97
|
+
- MongoDB: PROD_HOST, PROD_USERNAME, PROD_PASSWORD, PROD_AUTH_DATABASE, PROD_DB
|
|
98
|
+
STAGING_HOST, STAGING_USERNAME, STAGING_PASSWORD, STAGING_AUTH_DATABASE, STAGING_DB
|
|
99
|
+
- OCI/Rclone: STORAGE_REMOTE_NAME, STORAGE_TYPE, OCI_COMPARTMENT, OCI_NAMESPACE, OCI_REGION
|
|
100
|
+
OCI_USER, OCI_FINGERPRINT, OCI_KEY_FILE, OCI_TENANCY
|
|
101
|
+
- Paths: STORAGE_SOURCE, STORAGE_DEST
|
|
102
|
+
- Artifacts: ARTIFACTS_BUCKET (required for --upload-artifacts)
|
|
103
|
+
ARTIFACTS_PREFIX (optional, default: "env-sync")
|
|
104
|
+
- Optional: EXCLUDE_COLLECTIONS (space-separated list of collections to exclude)
|
|
105
|
+
BACKUP_RETENTION_DAYS (default: 7)
|
|
106
|
+
|
|
107
|
+
BACKUP METADATA
|
|
108
|
+
Backup metadata is stored in backups/.backup_metadata and tracks:
|
|
109
|
+
- Last backup timestamp
|
|
110
|
+
`);
|
|
111
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface MongoConfig {
|
|
2
|
+
authDatabase: string;
|
|
3
|
+
database: string;
|
|
4
|
+
host: string;
|
|
5
|
+
password: string;
|
|
6
|
+
username: string;
|
|
7
|
+
}
|
|
8
|
+
export interface StorageConfig {
|
|
9
|
+
compartment: string;
|
|
10
|
+
dest: string;
|
|
11
|
+
fingerprint: string;
|
|
12
|
+
keyFile: string;
|
|
13
|
+
namespace: string;
|
|
14
|
+
region: string;
|
|
15
|
+
remoteName: string;
|
|
16
|
+
source: string;
|
|
17
|
+
tenancy: string;
|
|
18
|
+
type: string;
|
|
19
|
+
user: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ArtifactsConfig {
|
|
22
|
+
/** OCI bucket name for storing artifacts */
|
|
23
|
+
bucket: string;
|
|
24
|
+
/** Optional prefix/folder within the bucket */
|
|
25
|
+
prefix: string;
|
|
26
|
+
}
|
|
27
|
+
export interface SyncConfig {
|
|
28
|
+
artifacts: ArtifactsConfig;
|
|
29
|
+
backupDir: string;
|
|
30
|
+
backupRetentionDays: number;
|
|
31
|
+
databaseProduction: MongoConfig;
|
|
32
|
+
databaseStaging: MongoConfig;
|
|
33
|
+
excludeCollections: string[];
|
|
34
|
+
scriptDir: string;
|
|
35
|
+
storage: StorageConfig;
|
|
36
|
+
}
|
|
37
|
+
export declare function loadConfig(): SyncConfig;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
function parseEnvValue(value, defaultValue = '') {
|
|
5
|
+
return value?.trim() || defaultValue;
|
|
6
|
+
}
|
|
7
|
+
function parseEnvNumber(value, defaultValue) {
|
|
8
|
+
if (!value)
|
|
9
|
+
return defaultValue;
|
|
10
|
+
const parsed = parseInt(value.trim(), 10);
|
|
11
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
12
|
+
}
|
|
13
|
+
export function loadConfig() {
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const scriptDir = path.resolve(__dirname, '../..');
|
|
17
|
+
const envFile = path.join(scriptDir, '.env');
|
|
18
|
+
if (!existsSync(envFile)) {
|
|
19
|
+
throw new Error(`Environment file not found at ${envFile}\nPlease copy env.example to .env and configure it`);
|
|
20
|
+
}
|
|
21
|
+
// Load .env file manually (dotenv doesn't work well with ESM in this context)
|
|
22
|
+
const envContent = readFileSync(envFile, 'utf-8');
|
|
23
|
+
const envVars = {};
|
|
24
|
+
for (const line of envContent.split('\n')) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
27
|
+
continue;
|
|
28
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
29
|
+
if (match) {
|
|
30
|
+
const key = match[1].trim();
|
|
31
|
+
const value = match[2].trim().replace(/^["']|["']$/g, '');
|
|
32
|
+
envVars[key] = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Validate required MongoDB variables
|
|
36
|
+
const requiredMongoVars = ['PROD_HOST', 'STAGING_HOST'];
|
|
37
|
+
for (const varName of requiredMongoVars) {
|
|
38
|
+
if (!envVars[varName]) {
|
|
39
|
+
throw new Error(`MongoDB configuration missing: ${varName} is required in ${envFile}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Validate required Storage variables
|
|
43
|
+
const requiredStorageVars = ['STORAGE_SOURCE', 'STORAGE_DEST', 'STORAGE_REMOTE_NAME', 'STORAGE_TYPE', 'OCI_COMPARTMENT', 'OCI_FINGERPRINT', 'OCI_KEY_FILE', 'OCI_NAMESPACE', 'OCI_REGION', 'OCI_TENANCY', 'OCI_USER'];
|
|
44
|
+
for (const varName of requiredStorageVars) {
|
|
45
|
+
if (!envVars[varName]) {
|
|
46
|
+
throw new Error(`Storage configuration missing: ${varName} is required in ${envFile}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const backupDir = path.join(scriptDir, 'backups');
|
|
50
|
+
return {
|
|
51
|
+
artifacts: {
|
|
52
|
+
bucket: parseEnvValue(envVars.ARTIFACTS_BUCKET, ''),
|
|
53
|
+
prefix: parseEnvValue(envVars.ARTIFACTS_PREFIX, ''),
|
|
54
|
+
},
|
|
55
|
+
backupDir,
|
|
56
|
+
backupRetentionDays: parseEnvNumber(envVars.BACKUP_RETENTION_DAYS, 7),
|
|
57
|
+
databaseProduction: {
|
|
58
|
+
authDatabase: parseEnvValue(envVars.PROD_AUTH_DATABASE, 'admin'),
|
|
59
|
+
database: parseEnvValue(envVars.PROD_DB),
|
|
60
|
+
host: parseEnvValue(envVars.PROD_HOST),
|
|
61
|
+
password: parseEnvValue(envVars.PROD_PASSWORD),
|
|
62
|
+
username: parseEnvValue(envVars.PROD_USERNAME),
|
|
63
|
+
},
|
|
64
|
+
databaseStaging: {
|
|
65
|
+
authDatabase: parseEnvValue(envVars.STAGING_AUTH_DATABASE, 'admin'),
|
|
66
|
+
database: parseEnvValue(envVars.STAGING_DB),
|
|
67
|
+
host: parseEnvValue(envVars.STAGING_HOST),
|
|
68
|
+
password: parseEnvValue(envVars.STAGING_PASSWORD),
|
|
69
|
+
username: parseEnvValue(envVars.STAGING_USERNAME),
|
|
70
|
+
},
|
|
71
|
+
excludeCollections: parseEnvValue(envVars.EXCLUDE_COLLECTIONS, '')
|
|
72
|
+
.split(/\s+/)
|
|
73
|
+
.filter(c => c.length > 0),
|
|
74
|
+
scriptDir,
|
|
75
|
+
storage: {
|
|
76
|
+
//
|
|
77
|
+
dest: parseEnvValue(envVars.STORAGE_DEST),
|
|
78
|
+
remoteName: parseEnvValue(envVars.STORAGE_REMOTE_NAME),
|
|
79
|
+
source: parseEnvValue(envVars.STORAGE_SOURCE),
|
|
80
|
+
type: parseEnvValue(envVars.STORAGE_TYPE),
|
|
81
|
+
//
|
|
82
|
+
compartment: parseEnvValue(envVars.OCI_COMPARTMENT),
|
|
83
|
+
fingerprint: parseEnvValue(envVars.OCI_FINGERPRINT),
|
|
84
|
+
keyFile: parseEnvValue(envVars.OCI_KEY_FILE),
|
|
85
|
+
namespace: parseEnvValue(envVars.OCI_NAMESPACE),
|
|
86
|
+
region: parseEnvValue(envVars.OCI_REGION),
|
|
87
|
+
tenancy: parseEnvValue(envVars.OCI_TENANCY),
|
|
88
|
+
user: parseEnvValue(envVars.OCI_USER),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
package/dist/index.d.ts
ADDED