Skip to content

Commit

Permalink
feat: global config sepc (#1725)
Browse files Browse the repository at this point in the history
* feat: initial work done to rely on new global config dir

* feat: add final functionality to the new global config spec

* chore: add comment to tests

* fix: typo in comment

Co-authored-by: Eduardo Bouças <mail@eduardoboucas.com>

* fix: don't fail tests if there's no config to backup

* fix: make tests resilient to missing config directories

* fix: trolled by nodejs fs functions 🤦

* chore: don't delete legacy config and memoise globalConfig result

* chore: dropping lodash.once as per #1728 and using memoize-one

* chore: dropping lodash.once as per #1728 and using memoize-one

* fix: require memoizeOne 🤦

Co-authored-by: Eduardo Bouças <mail@eduardoboucas.com>
  • Loading branch information
JGAntunes and eduardoboucas authored Jan 20, 2021
1 parent b400036 commit 3d8dd28
Show file tree
Hide file tree
Showing 16 changed files with 153 additions and 36 deletions.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"del": "^5.1.0",
"dot-prop": "^5.1.0",
"dotenv": "^8.2.0",
"env-paths": "^2.2.0",
"envinfo": "^7.3.1",
"execa": "^5.0.0",
"express": "^4.17.1",
Expand All @@ -129,6 +130,7 @@
"lodash": "^4.17.20",
"log-symbols": "^3.0.0",
"make-dir": "^3.0.0",
"memoize-one": "^5.1.1",
"minimist": "^1.2.5",
"multiparty": "^4.2.1",
"netlify": "^6.0.0",
Expand Down
3 changes: 2 additions & 1 deletion src/commands/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ class BuildCommand extends Command {
// Run Netlify Build
async run() {
// Retrieve Netlify Build options
const [token] = await this.getConfigToken()
const options = await getBuildOptions({
context: this,
token: this.getConfigToken()[0],
token,
flags: this.parse(BuildCommand).flags,
})
this.checkOptions(options)
Expand Down
3 changes: 2 additions & 1 deletion src/commands/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,10 @@ class DeployCommand extends Command {
}

if (flags.build) {
const [token] = await this.getConfigToken()
const options = await getBuildOptions({
context: this,
token: this.getConfigToken()[0],
token,
flags,
})
const exitCode = await runBuild(options)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const Command = require('../utils/command')
class LoginCommand extends Command {
async run() {
const { flags } = this.parse(LoginCommand)
const [accessToken, location] = this.getConfigToken()
const [accessToken, location] = await this.getConfigToken()
if (accessToken && !flags.new) {
this.log(`Already logged in ${msg(location)}`)
this.log()
Expand Down
2 changes: 1 addition & 1 deletion src/commands/logout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { track } = require('../utils/telemetry')

class LogoutCommand extends Command {
async run() {
const [accessToken, location] = this.getConfigToken()
const [accessToken, location] = await this.getConfigToken()

if (!accessToken) {
this.log(`Already logged out`)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/status/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class StatusCommand extends Command {
const { flags } = this.parse(StatusCommand)

const current = globalConfig.get('userId')
const [accessToken] = this.getConfigToken()
const [accessToken] = await this.getConfigToken()

if (!accessToken) {
this.log(`Not logged in. Please log in to see site status.`)
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ const process = require('process')

const envinfo = require('envinfo')

const globalConfig = require('../utils/global-config')
const getGlobalConfig = require('../utils/get-global-config')
const header = require('../utils/header')
const { track } = require('../utils/telemetry')

module.exports = async function initHooks(context) {
const globalConfig = await getGlobalConfig()
// Enable/disable telemetry Global flags. TODO refactor where these fire
if (context.id === '--telemetry-disable') {
globalConfig.set('telemetryDisabled', true)
Expand Down
4 changes: 4 additions & 0 deletions src/lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const pathType = require('path-type')
const statAsync = promisify(fs.stat)
const readFileAsync = promisify(fs.readFile)
const writeFileAsync = promisify(fs.writeFile)
const rmFileAsync = promisify(fs.unlink)
const copyFileAsync = promisify(fs.copyFile)
const accessAsync = promisify(fs.access)

const readFileAsyncCatchError = async (filepath) => {
Expand Down Expand Up @@ -39,6 +41,8 @@ module.exports = {
readFileAsync,
readFileAsyncCatchError,
writeFileAsync,
rmFileAsync,
copyFileAsync,
fileExistsAsync,
isFileAsync,
mkdirRecursiveSync,
Expand Down
13 changes: 10 additions & 3 deletions src/lib/settings.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
const os = require('os')
const path = require('path')

const envPaths = require('env-paths')

const OSBasedPaths = envPaths('netlify', { suffix: '' })
const NETLIFY_HOME = '.netlify'

const getHomeDirectory = () => path.join(os.homedir(), NETLIFY_HOME)
// Deprecated method to get netlify's home config - ~/.netlify/...
const getLegacyPathInHome = (paths) => {
const pathInHome = path.join(os.homedir(), NETLIFY_HOME, ...paths)
return pathInHome
}

const getPathInHome = (paths) => {
const pathInHome = path.join(getHomeDirectory(), ...paths)
const pathInHome = path.join(OSBasedPaths.config, ...paths)
return pathInHome
}

Expand All @@ -15,4 +22,4 @@ const getPathInProject = (paths) => {
return pathInProject
}

module.exports = { getPathInHome, getPathInProject }
module.exports = { getLegacyPathInHome, getPathInHome, getPathInProject }
15 changes: 9 additions & 6 deletions src/utils/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const { warnOnNetlifyDir } = require('../lib/deprecations')
const { getAgent } = require('../lib/http-agent')

const chalkInstance = require('./chalk')
const globalConfig = require('./global-config')
const getGlobalConfig = require('./get-global-config')
const openBrowser = require('./open-browser')
const StateConfig = require('./state-config')
const { track, identify } = require('./telemetry')
Expand All @@ -30,7 +30,7 @@ const isDefaultJson = () => argv._[0] === 'functions:invoke' || (argv._[0] === '

const isBuildCommand = () => argv._[0] === 'build' || (argv._[0] === 'deploy' && argv.build === true)

const getToken = (tokenFromFlag) => {
const getToken = async (tokenFromFlag) => {
// 1. First honor command flag --auth
if (tokenFromFlag) {
return [tokenFromFlag, 'flag']
Expand All @@ -40,6 +40,7 @@ const getToken = (tokenFromFlag) => {
return [NETLIFY_AUTH_TOKEN, 'env']
}
// 3. If no env var use global user setting
const globalConfig = await getGlobalConfig()
const userId = globalConfig.get('userId')
const tokenFromConfig = globalConfig.get(`users.${userId}.auth.token`)
if (tokenFromConfig) {
Expand All @@ -55,7 +56,7 @@ class BaseCommand extends Command {
// Grab netlify API token
const authViaFlag = getAuthArg(argv)

const [token] = this.getConfigToken(authViaFlag)
const [token] = await this.getConfigToken(authViaFlag)

// Get site id & build state
const state = new StateConfig(cwd)
Expand All @@ -78,6 +79,8 @@ class BaseCommand extends Command {
apiOpts.pathPrefix = NETLIFY_API_URL === `${apiUrl.protocol}//${apiUrl.host}` ? '/api/v1' : apiUrl.pathname
}

const globalConfig = await getGlobalConfig()

this.netlify = {
// api methods
api: new API(token || '', apiOpts),
Expand Down Expand Up @@ -205,14 +208,14 @@ class BaseCommand extends Command {
/**
* Get user netlify API token
* @param {string} - [tokenFromFlag] - value passed in by CLI flag
* @return {[string, string]} - tokenValue & location of resolved Netlify API token
* @return {Promise<[string, string]>} - Promise containing tokenValue & location of resolved Netlify API token
*/
getConfigToken(tokenFromFlag) {
return getToken(tokenFromFlag)
}

authenticate(tokenFromFlag) {
const [token] = this.getConfigToken(tokenFromFlag)
async authenticate(tokenFromFlag) {
const [token] = await this.getConfigToken(tokenFromFlag)
if (token) {
return token
}
Expand Down
32 changes: 32 additions & 0 deletions src/utils/get-global-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const Configstore = require('configstore')
const memoizeOne = require('memoize-one')
const { v4: uuidv4 } = require('uuid')

const { readFileAsync } = require('../lib/fs')
const { getPathInHome, getLegacyPathInHome } = require('../lib/settings')

const globalConfigDefaults = {
/* disable stats from being sent to Netlify */
telemetryDisabled: false,
/* cliId */
cliId: uuidv4(),
}

const getGlobalConfig = async function () {
const configPath = getPathInHome(['config.json'])
// Legacy config file in home ~/.netlify/config.json
const legacyPath = getLegacyPathInHome(['config.json'])
let legacyConfig
// Read legacy config if exists
try {
legacyConfig = JSON.parse(await readFileAsync(legacyPath))
} catch (_) {}
// Use legacy config as default values
const defaults = { ...globalConfigDefaults, ...legacyConfig }
const configStore = new Configstore(null, defaults, { configPath })

return configStore
}

// Memoise config result so that we only load it once
module.exports = memoizeOne(getGlobalConfig)
76 changes: 76 additions & 0 deletions src/utils/get-global-config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const os = require('os')
const path = require('path')

const test = require('ava')

const {
rmdirRecursiveAsync,
mkdirRecursiveAsync,
readFileAsync,
writeFileAsync,
copyFileAsync,
rmFileAsync,
} = require('../lib/fs')
const { getPathInHome, getLegacyPathInHome } = require('../lib/settings')

const getGlobalConfig = require('./get-global-config.js')

const configPath = getPathInHome(['config.json'])
const legacyConfigPath = getLegacyPathInHome(['config.json'])
const tmpConfigBackupPath = path.join(os.tmpdir(), `netlify-config-backup-${Date.now()}`)

test.before('backup current user config if exists', async () => {
try {
await copyFileAsync(configPath, tmpConfigBackupPath)
} catch (_) {}
})

test.after.always('cleanup tmp directory and legacy config', async () => {
try {
// Restore user config if exists
await mkdirRecursiveAsync(getPathInHome([]))
await copyFileAsync(tmpConfigBackupPath, configPath)
// Remove tmp backup if exists
await rmFileAsync(tmpConfigBackupPath)
} catch (_) {}
// Remove legacy config path
await rmdirRecursiveAsync(getLegacyPathInHome([]))
})

test.beforeEach('recreate clean config directories', async () => {
// Remove config dirs
await rmdirRecursiveAsync(getPathInHome([]))
await rmdirRecursiveAsync(getLegacyPathInHome([]))
// Make config dirs
await mkdirRecursiveAsync(getPathInHome([]))
await mkdirRecursiveAsync(getLegacyPathInHome([]))
})

// Not running tests in parallel as we're messing with the same config files

test.serial('should use legacy config values as default if exists', async (t) => {
const legacyConfig = { someOldKey: 'someOldValue', overrideMe: 'oldValue' }
const newConfig = { overrideMe: 'newValue' }
await writeFileAsync(legacyConfigPath, JSON.stringify(legacyConfig))
await writeFileAsync(configPath, JSON.stringify(newConfig))

const globalConfig = await getGlobalConfig()
t.is(globalConfig.get('someOldKey'), legacyConfig.someOldKey)
t.is(globalConfig.get('overrideMe'), newConfig.overrideMe)
})

test.serial('should not throw if legacy config is invalid JSON', async (t) => {
await writeFileAsync(legacyConfigPath, 'NotJson')
await t.notThrowsAsync(getGlobalConfig)
})

test.serial("should create config in netlify's config dir if none exists and store new values", async (t) => {
// Remove config dirs
await rmdirRecursiveAsync(getPathInHome([]))
await rmdirRecursiveAsync(getLegacyPathInHome([]))

const globalConfig = await getGlobalConfig()
globalConfig.set('newProp', 'newValue')
const configFile = JSON.parse(await readFileAsync(configPath))
t.deepEqual(globalConfig.all, configFile)
})
17 changes: 0 additions & 17 deletions src/utils/global-config.js

This file was deleted.

8 changes: 5 additions & 3 deletions src/utils/telemetry/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const process = require('process')

const ci = require('ci-info')

const globalConfig = require('../global-config')
const getGlobalConfig = require('../get-global-config')

const isValidEventName = require('./validation')

Expand Down Expand Up @@ -45,7 +45,7 @@ const eventConfig = {
],
}

const track = function (eventName, payload) {
const track = async function (eventName, payload) {
const properties = payload || {}

if (IS_INSIDE_CI) {
Expand All @@ -55,6 +55,7 @@ const track = function (eventName, payload) {
return Promise.resolve()
}

const globalConfig = await getGlobalConfig()
// exit early if tracking disabled
const TELEMETRY_DISABLED = globalConfig.get('telemetryDisabled')
if (TELEMETRY_DISABLED && !properties.force) {
Expand Down Expand Up @@ -103,7 +104,7 @@ const track = function (eventName, payload) {
return send('track', defaultData)
}

const identify = function (payload) {
const identify = async function (payload) {
const data = payload || {}

if (IS_INSIDE_CI) {
Expand All @@ -113,6 +114,7 @@ const identify = function (payload) {
return Promise.resolve()
}

const globalConfig = await getGlobalConfig()
// exit early if tracking disabled
const TELEMETRY_DISABLED = globalConfig.get('telemetryDisabled')
if (TELEMETRY_DISABLED && !data.force) {
Expand Down
2 changes: 1 addition & 1 deletion tests/command.deploy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ if (process.env.IS_FORK !== 'true') {

// validate edge handlers
// use this until we can use `netlify api`
const [apiToken] = getToken()
const [apiToken] = await getToken()
const { content_length: contentLength, ...rest } = await got(
`https://api.netlify.com/api/v1/deploys/${deploy.deploy_id}/edge_handlers`,
{
Expand Down

0 comments on commit 3d8dd28

Please sign in to comment.