Skip to content

Creating an NA <-> EU migration option #3005

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/cmds/migrate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
exports.command = 'migrate <command>';
exports.desc = 'Migration utilities';
exports.builder = function (yargs) {
return yargs.commandDir('migrate_cmds');
};
194 changes: 194 additions & 0 deletions lib/cmds/migrate_cmds/datacenter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
const path = require('path')
const fs = require('fs')
const os = require('os')
const runContentfulExport = require('contentful-export')
const runContentfulImport = require('contentful-import')
const { handleAsyncError: handle } = require('../../utils/async')
const { version } = require('../../../package.json')
const logging = require('../../utils/log')
const { getHeadersFromOption } = require('../../utils/headers')
const emojic = require('emojic')

const HOST_MAP = {
eu: 'api.eu.contentful.com',
na: 'api.contentful.com'
}

const ensureDirExists = dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
}

const migrateDataCenter = async argv => {
const {
source,
target,
sourceSpaceId,
targetSpaceId,
sourceToken,
targetToken,
environmentId,
useVerboseRenderer
} = argv

const exportDir = path.join(process.cwd(), 'tmp-export')
ensureDirExists(exportDir)

if (argv.includeTaxonomies) {
if (!argv.sourceOrgId || !argv.targetOrgId) {
throw new Error(
'--source-org-id and --target-org-id are required when --include-taxonomies is set'
)
}

const taxonomyFile = path.join(
exportDir,
`taxonomy-${argv.sourceOrgId}-${Date.now()}.json`
)

console.log(
`Exporting taxonomies from source organization ${argv.sourceOrgId}...`
)

const {
organizationExport
} = require('../../cmds/organization_cmds/export')

await organizationExport({
context: { managementToken: argv.sourceToken },
organizationId: argv.sourceOrgId,
header: argv.header,
outputFile: taxonomyFile,
saveFile: true,
host: HOST_MAP[target]
})

logging.success(`${emojic.whiteCheckMark} Taxonomy export complete`)

console.log(
`${emojic.inboxTray} Importing taxonomies into target organization ${argv.targetOrgId}...`
)

const {
organizationImport
} = require('../../cmds/organization_cmds/import')

await organizationImport({
context: { managementToken: argv.targetToken },
organizationId: argv.targetOrgId,
header: argv.header,
contentFile: taxonomyFile,
silent: !argv.useVerboseRenderer,
host: HOST_MAP[target]
})

logging.success(`${emojic.whiteCheckMark} Taxonomy import complete`)
}

const contentFile = path.join(
exportDir,
`contentful-export-${sourceSpaceId}-${environmentId}-${Date.now()}.json`
)

console.log(`🚚 Exporting contentful data from [${source}]...`)
await runContentfulExport({
spaceId: sourceSpaceId,
environmentId,
managementToken: sourceToken,
host: HOST_MAP[source],
contentFile,
saveFile: true,
useVerboseRenderer,
managementApplication: `contentful.cli/${version}`,
managementFeature: 'migrate-datacenter',
headers: getHeadersFromOption(argv.header)
})

logging.success(`${emojic.whiteCheckMark} Export complete`)

console.log(`${emojic.inboxTray} Importing content into [${target}]...`)
await runContentfulImport({
spaceId: targetSpaceId,
environmentId,
managementToken: targetToken,
host: HOST_MAP[target],
contentFile,
useVerboseRenderer,
managementApplication: `contentful.cli/${version}`,
managementFeature: 'migrate-datacenter',
headers: getHeadersFromOption(argv.header)
})

logging.success(`${emojic.tada} Migration complete!`)
}

module.exports.command = 'datacenter'
module.exports.desc = 'Migrate a space between Contentful data centers'
module.exports.builder = yargs =>
yargs
.option('source-dc', {
alias: 'source',
describe: 'Source data center (na or eu)',
choices: ['na', 'eu'],
demandOption: true
})
.option('target-dc', {
alias: 'target',
describe: 'Target data center (na or eu)',
choices: ['na', 'eu'],
demandOption: true
})
.option('source-space-id', {
describe: 'Source space ID',
type: 'string',
demandOption: true
})
.option('target-space-id', {
describe: 'Target space ID',
type: 'string',
demandOption: true
})
.option('environment-id', {
describe: 'Environment ID (e.g. master)',
type: 'string',
default: 'master'
})
.option('source-token', {
describe: 'Source CMA token',
type: 'string',
demandOption: true
})
.option('target-token', {
describe: 'Target CMA token',
type: 'string',
demandOption: true
})
.option('use-verbose-renderer', {
describe: 'Display progress in new lines instead of spinner',
type: 'boolean',
default: false
})
.option('include-taxonomies', {
describe:
'Migrate taxonomies (Concepts & Concept Schemes) before importing content',
type: 'boolean',
default: false
})
.option('source-org-id', {
describe: 'Organization ID where taxonomies should be exported from',
type: 'string'
})
.option('target-org-id', {
describe: 'Organization ID where taxonomies should be imported to',
type: 'string'
})
.option('skip-content-publishing', {
describe:
'Skips content publishing. Creates content but does not publish it',
type: 'boolean',
default: false
})

module.exports.handler = handle(migrateDataCenter)
module.exports.migrateDataCenter = migrateDataCenter
13 changes: 11 additions & 2 deletions lib/cmds/organization_cmds/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface OrgImportParams {
contentFile: string
silent?: boolean
errorLogFile?: string
host?: string
}

export interface OrgImportContext {
Expand All @@ -85,15 +86,23 @@ interface ErrorMessage {
}

async function importCommand(params: OrgImportParams) {
const { context, header, organizationId, contentFile, silent, errorLogFile } =
const { context, header, organizationId, contentFile, silent, errorLogFile, host } =
params
const { managementToken } = context

console.log(
'👉 CMA Host being used for taxonomy import:',
host || 'api.contentful.com'
)
console.log('👉 Token prefix:', managementToken)
console.log('👉 Organization ID:', organizationId)

const cmaClient = await createPlainClient({
accessToken: managementToken,
feature: 'org-import',
headers: getHeadersFromOption(header),
logHandler: noop
logHandler: noop,
host
})

const importContext: OrgImportContext = {
Expand Down
17 changes: 13 additions & 4 deletions lib/utils/contentful-clients.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@ async function createManagementClient(params) {
}

async function createPlainClient(params, defaults = {}) {
params.application = `contentful.cli/${version}`

const context = await getContext()
const { rawProxy, proxy, host, insecure } = context
const { rawProxy, proxy, host: contextHost, insecure } = context

const proxyConfig = {}
if (!rawProxy) {
Expand All @@ -34,8 +32,19 @@ async function createPlainClient(params, defaults = {}) {
proxyConfig.proxy = proxy
}

// Added this to ensure that the host is set correctly, currently we rely on a json file that sets it, this allows us to migrate and pass a flag if set for the migrate option
const effectiveHost = params.host || contextHost || 'api.contentful.com'

console.log('🌐 Final CMA host:', effectiveHost)

return createClient(
{ ...params, ...proxyConfig, host, insecure },
{
...params,
...proxyConfig,
host: effectiveHost,
insecure,
application: `contentful.cli/${version}`
},
{ type: 'plain', defaults }
)
}
Expand Down
Loading