Skip to content

Commit

Permalink
feat: support env write operations for sites opted in to beta env var…
Browse files Browse the repository at this point in the history
… experience (#4751)

* feat: support envelope in env:set

* chore: refactor

* feat: support envelope in env:unset

* feat: support envelope in env:import

* chore: add param defaults to helper methods

* fix: safer assignment in set command if env is nil

* fix: imported env should always take precedence in case of name collision

* feat: support envelope in env:migrate

* test: pass integration test locally

* test: adding unit tests

* chore: update contributors field

* chore: update contributors field

* test: adding integration tests

* chore: adding headerdoc

* chore: update contributors field

* chore: update contributors field

* chore: update parameter name in docs markdown file

* chore: update contributors field

* chore: update contributors field

* fix: addressing PR feedback

* chore: update contributors field

* chore: update contributors field

Co-authored-by: Jason Barry <[email protected]>
Co-authored-by: jasonbarry <[email protected]>
Co-authored-by: token-generator-app[bot] <token-generator-app[bot]@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
5 people authored Jun 30, 2022
1 parent 7dedadc commit e0695f9
Show file tree
Hide file tree
Showing 11 changed files with 910 additions and 61 deletions.
4 changes: 2 additions & 2 deletions docs/commands/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ netlify env:set

**Arguments**

- name - Environment variable name
- key - Environment variable key
- value - Value to set to

**Flags**
Expand All @@ -162,7 +162,7 @@ netlify env:unset

**Arguments**

- name - Environment variable name
- key - Environment variable key

**Flags**

Expand Down
86 changes: 66 additions & 20 deletions src/commands/env/env-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const AsciiTable = require('ascii-table')
const dotenv = require('dotenv')
const isEmpty = require('lodash/isEmpty')

const { exit, log, logJson } = require('../../utils')
const { exit, log, logJson, translateFromEnvelopeToMongo, translateFromMongoToEnvelope } = require('../../utils')

/**
* The env:import command
Expand All @@ -23,13 +23,6 @@ const envImport = async (fileName, options, command) => {
return false
}

const siteData = await api.getSite({ siteId })

// Get current environment variables set in the UI
const {
build_settings: { env = {} },
} = siteData

let importedEnv = {}
try {
const envFileContents = await readFile(fileName, 'utf-8')
Expand All @@ -44,21 +37,14 @@ const envImport = async (fileName, options, command) => {
return false
}

// Apply environment variable updates
const siteResult = await api.updateSite({
siteId,
body: {
build_settings: {
// Only set imported variables if --replaceExisting or otherwise merge
// imported ones with the current environment variables.
env: options.replaceExisting ? importedEnv : { ...env, ...importedEnv },
},
},
})
const siteData = await api.getSite({ siteId })

const importIntoService = siteData.use_envelope ? importIntoEnvelope : importIntoMongo
const finalEnv = await importIntoService({ api, importedEnv, options, siteData })

// Return new environment variables of site if using json flag
if (options.json) {
logJson(siteResult.build_settings.env)
logJson(finalEnv)
return false
}

Expand All @@ -71,6 +57,66 @@ const envImport = async (fileName, options, command) => {
log(table.toString())
}

/**
* Updates the imported env in the site record
* @returns {Promise<object>}
*/
const importIntoMongo = async ({ api, importedEnv, options, siteData }) => {
const { env = {} } = siteData.build_settings
const siteId = siteData.id

const finalEnv = options.replaceExisting ? importedEnv : { ...env, ...importedEnv }

// Apply environment variable updates
await api.updateSite({
siteId,
body: {
build_settings: {
// Only set imported variables if --replaceExisting or otherwise merge
// imported ones with the current environment variables.
env: finalEnv,
},
},
})

return finalEnv
}

/**
* Saves the imported env in the Envelope service
* @returns {Promise<object>}
*/
const importIntoEnvelope = async ({ api, importedEnv, options, siteData }) => {
// fetch env vars
const accountId = siteData.account_slug
const siteId = siteData.id
const dotEnvKeys = Object.keys(importedEnv)
const envelopeVariables = await api.getEnvVars({ accountId, siteId })
const envelopeKeys = envelopeVariables.map(({ key }) => key)

// if user intends to replace all existing env vars
// either replace; delete all existing env vars on the site
// or, merge; delete only the existing env vars that would collide with new .env entries
const keysToDelete = options.replaceExisting ? envelopeKeys : envelopeKeys.filter((key) => dotEnvKeys.includes(key))

// delete marked env vars in parallel
await Promise.all(keysToDelete.map((key) => api.deleteEnvVar({ accountId, siteId, key })))

// hit create endpoint
const body = translateFromMongoToEnvelope(importedEnv)
try {
await api.createEnvVars({ accountId, siteId, body })
} catch (error) {
throw error.json ? error.json.msg : error
}

// return final env to aid in --json output (for testing)
return {
...translateFromEnvelopeToMongo(envelopeVariables.filter(({ key }) => !keysToDelete.includes(key))),
...importedEnv,
}
}

/**
* Creates the `netlify env:import` command
* @param {import('../base-command').BaseCommand} program
Expand Down
147 changes: 140 additions & 7 deletions src/commands/env/env-migrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

const { isEmpty } = require('lodash')

const { chalk, error: logError, log } = require('../../utils')
const {
chalk,
error: logError,
log,
translateFromEnvelopeToMongo,
translateFromMongoToEnvelope,
} = require('../../utils')

const safeGetSite = async (api, siteId) => {
try {
Expand Down Expand Up @@ -51,6 +57,37 @@ const envMigrate = async (options, command) => {
return false
}

// determine if siteFrom and/or siteTo is on Envelope
let method
if (!siteFrom.use_envelope && !siteTo.use_envelope) {
method = mongoToMongo
} else if (!siteFrom.use_envelope && siteTo.use_envelope) {
method = mongoToEnvelope
} else if (siteFrom.use_envelope && !siteTo.use_envelope) {
method = envelopeToMongo
} else {
method = envelopeToEnvelope
}
const success = await method({ api, siteFrom, siteTo })

if (!success) {
return false
}

log(
`Successfully migrated environment variables from ${chalk.greenBright(siteFrom.name)} to ${chalk.greenBright(
siteTo.name,
)}`,
)

return true
}

/**
* Copies the env from a site not configured with Envelope to a different site not configured with Envelope
* @returns {Promise<boolean>}
*/
const mongoToMongo = async ({ api, siteFrom, siteTo }) => {
const [
{
build_settings: { env: envFrom = {} },
Expand All @@ -73,19 +110,115 @@ const envMigrate = async (options, command) => {

// Apply environment variable updates
await api.updateSite({
siteId: siteId.to,
siteId: siteTo.id,
body: {
build_settings: {
env: mergedEnv,
},
},
})

log(
`Successfully migrated environment variables from ${chalk.greenBright(siteFrom.name)} to ${chalk.greenBright(
siteTo.name,
)}`,
)
return true
}

/**
* Copies the env from a site not configured with Envelope to a site configured with Envelope
* @returns {Promise<boolean>}
*/
const mongoToEnvelope = async ({ api, siteFrom, siteTo }) => {
const envFrom = siteFrom.build_settings.env || {}
const keysFrom = Object.keys(envFrom)

if (isEmpty(envFrom)) {
log(`${chalk.greenBright(siteFrom.name)} has no environment variables, nothing to migrate`)
return false
}

const accountId = siteTo.account_slug
const siteId = siteTo.id

const envelopeTo = await api.getEnvVars({ accountId, siteId })

const envVarsToDelete = envelopeTo.filter(({ key }) => keysFrom.includes(key))
// delete marked env vars in parallel
await Promise.all(envVarsToDelete.map(({ key }) => api.deleteEnvVar({ accountId, siteId, key })))

// hit create endpoint
const body = translateFromMongoToEnvelope(envFrom)
try {
await api.createEnvVars({ accountId, siteId, body })
} catch (error) {
throw error.json ? error.json.msg : error
}

return true
}

/**
* Copies the env from a site configured with Envelope to a site not configured with Envelope
* @returns {Promise<boolean>}
*/
const envelopeToMongo = async ({ api, siteFrom, siteTo }) => {
const envelopeVariables = await api.getEnvVars({ accountId: siteFrom.account_slug, siteId: siteFrom.id })
const envFrom = translateFromEnvelopeToMongo(envelopeVariables)

if (isEmpty(envFrom)) {
log(`${chalk.greenBright(siteFrom.name)} has no environment variables, nothing to migrate`)
return false
}

const envTo = siteTo.build_settings.env || {}

// Merge from site A to site B
const mergedEnv = {
...envTo,
...envFrom,
}

// Apply environment variable updates
await api.updateSite({
siteId: siteTo.id,
body: {
build_settings: {
env: mergedEnv,
},
},
})

return true
}

/**
* Copies the env from a site configured with Envelope to a different site configured with Envelope
* @returns {Promise<boolean>}
*/
const envelopeToEnvelope = async ({ api, siteFrom, siteTo }) => {
const [envelopeFrom, envelopeTo] = await Promise.all([
api.getEnvVars({ accountId: siteFrom.account_slug, siteId: siteFrom.id }),
api.getEnvVars({ accountId: siteTo.account_slug, siteId: siteTo.id }),
])

const keysFrom = envelopeFrom.map(({ key }) => key)

if (isEmpty(keysFrom)) {
log(`${chalk.greenBright(siteFrom.name)} has no environment variables, nothing to migrate`)
return false
}

const accountId = siteTo.account_slug
const siteId = siteTo.id
const envVarsToDelete = envelopeTo.filter(({ key }) => keysFrom.includes(key))
// delete marked env vars in parallel
await Promise.all(envVarsToDelete.map(({ key }) => api.deleteEnvVar({ accountId, siteId, key })))

// hit create endpoint
try {
await api.createEnvVars({ accountId, siteId, body: envelopeFrom })
} catch (error) {
throw error.json ? error.json.msg : error
}

return true
}

/**
Expand Down
Loading

1 comment on commit e0695f9

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📊 Benchmark results

Package size: 221 MB

Please sign in to comment.