Skip to content

Commit

Permalink
add addon:config & update addon:create command
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidWells committed Feb 20, 2019
1 parent aa7c6ac commit 8adfa6f
Show file tree
Hide file tree
Showing 12 changed files with 492 additions and 39 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"clean-deep": "^3.0.2",
"cli-spinners": "^1.3.1",
"cli-ux": "^5.1.0",
"concordance": "^4.0.0",
"configstore": "^4.0.0",
"dot-prop": "^4.2.0",
"find-up": "^3.0.0",
Expand All @@ -69,6 +70,7 @@
"is-docker": "^1.1.0",
"lodash.get": "^4.4.2",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.1",
"lodash.mergewith": "^4.6.1",
"lodash.snakecase": "^4.1.1",
Expand Down
13 changes: 10 additions & 3 deletions src/commands/addons/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,16 @@ class AddonsConfigCommand extends Command {
// Get Existing Config
const currentConfig = currentAddon.config || {}

console.log(`Current "${addonName} add-on" config values:\n`)
render.configValues(currentConfig)
console.log()
const words = `Current "${addonName} add-on" Settings:`
console.log(` ${chalk.yellowBright.bold(words)}`)
if (manifest.config) {
render.configValues(addonName, manifest.config, currentConfig)
} else {
// For addons without manifest. TODO remove once we enfore manifests
Object.keys(currentConfig).forEach((key) => {
console.log(`${key} - ${currentConfig[key]}`)
})
}

if (manifest.config) {
const required = requiredConfigValues(manifest.config)
Expand Down
160 changes: 129 additions & 31 deletions src/commands/addons/create.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
const Command = require('../../base')
const { getAddons, createAddon } = require('netlify/src/addons')
const {
getAddons,
createAddon
} = require('netlify/src/addons')
const parseRawFlags = require('../../utils/parseRawFlags')
const getAddonManifest = require('../../utils/addons/api')
const {
requiredConfigValues,
missingConfigValues,
updateConfigValues
} = require('../../utils/addons/validation')
const generatePrompts = require('../../utils/addons/prompts')
const render = require('../../utils/addons/render')
const chalk = require('chalk')
const inquirer = require('inquirer')

class addonsCreateCommand extends Command {
async run() {
const accessToken = await this.authenticate()
const { args, raw } = this.parse(addonsCreateCommand)
const { api, site } = this.netlify
const {
args,
raw
} = this.parse(addonsCreateCommand)
const {
api,
site
} = this.netlify

const addonName = args.name

if (!addonName) {
this.log('Please provide an addon name to provision')
this.log('Please provide an add-on name to provision')
// this.log(util.inspect(myObject, false, null, true /* enable colors */))
this.exit()
}
Expand All @@ -23,56 +42,135 @@ class addonsCreateCommand extends Command {
return false
}

const siteData = await api.getSite({
siteId
})
// this.log(site)
const addons = await getAddons(siteId, accessToken)

if (typeof addons === 'object' && addons.error) {
this.log('API Error', addons)
return false
}

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

// Filter down addons to current args.name
const currentAddon = addons.find((addon) => addon.service_path === `/.netlify/${addonName}`)

if (currentAddon.id) {
this.log(`Addon ${addonName} already exists for ${siteData.name}`)
this.log(`> Run \`netlify addons:update ${addonName}\` to update settings`)
this.log(`> Run \`netlify addons:delete ${addonName}\` to delete this addon`)
// GET flags from `raw` data
const rawFlags = parseRawFlags(raw)

if (currentAddon && currentAddon.id) {
this.log(`The "${addonName} add-on" already exists for ${siteData.name}`)
this.log()
const cmd = chalk.cyan(`\`netlify addons:config ${addonName}\``)
this.log(`- To update this add-on run: ${cmd}`)
const deleteCmd = chalk.cyan(`\`netlify addons:delete ${addonName}\``)
this.log(`- To remove this add-on run: ${deleteCmd}`)
this.log()
return false
}

const settings = {
siteId: siteId,
addon: addonName,
config: parseRawFlags(raw)
const manifest = await getAddonManifest(addonName, accessToken)

let configValues = rawFlags
if (manifest.config) {
const required = requiredConfigValues(manifest.config)
const missingValues = missingConfigValues(required, rawFlags)
console.log(`Starting the setup for "${addonName} add-on"`)
console.log()

if (Object.keys(rawFlags).length) {
const newConfig = updateConfigValues(manifest.config, {}, rawFlags)

if (missingValues.length) {
/* Warn user of missing required values */
console.log(`${chalk.redBright.underline.bold(`Error: Missing required configuration for "${addonName} add-on"`)}`)
console.log(`Please supply the configuration values as CLI flags`)
console.log()
render.missingValues(missingValues, manifest)
console.log()
}


await createSiteAddon({
addonName,
settings: {
siteId: siteId,
addon: addonName,
config: newConfig
},
accessToken,
siteData
})
return false
}

const words = `The ${addonName} add-on has the following configurable options:`
console.log(` ${chalk.yellowBright.bold(words)}`)
render.configValues(addonName, manifest.config)
console.log()
console.log(` ${chalk.greenBright.bold('Lets configure those!')}`)

console.log()
console.log(` - Hit ${chalk.white.bold('enter')} to confirm value or set empty value`)
console.log(` - Hit ${chalk.white.bold('ctrl + C')} to cancel & exit configuration`)
console.log()

const prompts = generatePrompts({
config: manifest.config,
configValues: rawFlags,
})

const userInput = await inquirer.prompt(prompts)
// Merge user input with the flags specified
configValues = updateConfigValues(manifest.config, rawFlags, userInput)
}
const addonResponse = await createAddon(settings, accessToken)

if (addonResponse.code === 404) {
this.log(`No addon "${addonName}" found. Please double check your addon name and try again`)
return false
}
this.log(`Addon "${addonName}" created for ${siteData.name}`)
await createSiteAddon({
addonName,
settings: {
siteId: siteId,
addon: addonName,
config: configValues
},
accessToken,
siteData
})
}
}

addonsCreateCommand.description = `Add an addon extension to your site
async function createSiteAddon({
addonName,
settings,
accessToken,
siteData
}) {
const addonResponse = await createAddon(settings, accessToken)

if (addonResponse.code === 404) {
console.log(`No add-on "${addonName}" found. Please double check your add-on name and try again`)
return false
}
console.log(`Add-on "${addonName}" created for ${siteData.name}`)
if (addonResponse.config && addonResponse.config.message) {
console.log()
console.log(`${addonResponse.config.message}`)
}
return addonResponse
}

addonsCreateCommand.description = `Add an add-on extension to your site
...
Addons are a way to extend the functionality of your Netlify site
Add-ons are a way to extend the functionality of your Netlify site
`

addonsCreateCommand.args = [
{
name: 'name',
required: true,
description: 'addon namespace'
}
]
addonsCreateCommand.args = [{
name: 'name',
required: true,
description: 'Add-on namespace'
}]

// allow for any flags. Handy for variadic configuration options
addonsCreateCommand.strict = false

addonsCreateCommand.hidden = true

module.exports = addonsCreateCommand
7 changes: 4 additions & 3 deletions src/commands/open/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,19 @@ Run \`netlify link\` to connect to this folder to a site`)
this.log(`Opening "${siteData.name}" site admin UI:`)
this.log(`> ${siteData.admin_url}`)
} catch (e) {
console.log('e', e)
if (e.status === 401 /* unauthorized*/) {
this.warn(`Log in with a different account or re-link to a site you have permission for`)
this.error(`Not authorized to view the currently linked site (${siteId})`)
}
if (e.status === 404 /* site not found */) {
this.log(`No site with id ${siteId} found.`)

this.log()
this.log('Please double check this ID and verify you are logged in with the correct account')
this.log()
this.log('To fix this, run `netlify unlink` then `netlify link` to reconnect to the correct site ID')
this.log()
this.error(`Site (${siteId}) not found in account`)
this.error(`Site "${siteId}" not found in account`)

}
this.error(e)
}
Expand Down
9 changes: 7 additions & 2 deletions src/commands/unlink.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class UnlinkCommand extends Command {
const siteId = site.id

if (!siteId) {
this.log(`Folder is not linked to a Netlify site`)
this.log(`Folder is not linked to a Netlify site. Run 'netlify link' to link it`)
return this.exit()
}

Expand All @@ -24,7 +24,12 @@ class UnlinkCommand extends Command {
siteId: siteData.id || siteId,
})

this.log(`Unlinked ${site.configPath} from ${siteData ? siteData.name : siteId}`)
if (site) {
this.log(`Unlinked ${site.configPath} from ${siteData ? siteData.name : siteId}`)
} else {
this.log('Unlinked site')
}

}
}

Expand Down
22 changes: 22 additions & 0 deletions src/utils/addons/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const fetch = require('node-fetch')

async function getAddonManifest(addonName, netlifyApiToken) {
const url = `https://api.netlify.com/api/v1/services/${addonName}/manifest`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${netlifyApiToken}`
}
})

const data = await response.json()

if (response.status === 422) {
throw new Error(`Error ${JSON.stringify(data)}`)
}

return data
}

module.exports = getAddonManifest
51 changes: 51 additions & 0 deletions src/utils/addons/compare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const isEqual = require('lodash.isequal')

module.exports = function compare(oldValues, newValues) {
const initialData = {
// default everything is equal
isEqual: true,
// Keys that are different
keys: [],
// Values of the keys that are different
diffs: {}
}

const oldKeys = Object.keys(oldValues)
const newKeys = Object.keys(newValues)
const set = new Set(newKeys.concat(oldKeys))

return Array.from(set).reduce((acc, current) => {
// if values not deep equal. There are changes
if (!isEqual(newValues[current], oldValues[current])) {
return {
isEqual: false,
keys: acc.keys.concat(current),
diffs: Object.assign({}, acc.diffs, {
[`${current}`]: {
newValue: newValues[current],
oldValue: oldValues[current]
}
})
}
}
return acc
}, initialData)
}

// const newValues = {
// lol: 'byelol',
// }
// const oldValues = {
// lol: 'bye',
// cool: 'dddd'
// }

// const diffs = compareNewToOld(newValues, oldValues)
// console.log('diffs', diffs)

// const diff = diffValues(oldValues, newValues)
// if (diff) {
// console.log(`Config updates\n`)
// console.log(`${diff}\n`)
// // from '${currentState}' to '${newInput}'
// }
18 changes: 18 additions & 0 deletions src/utils/addons/diffs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const concordance = require('concordance')
const { concordanceOptions, concordanceDiffOptions } = require('./options')

function formatDescriptorDiff(actualDescriptor, expectedDescriptor, options) {
const diffOptions = Object.assign({}, options, concordanceDiffOptions)
return concordance.diffDescriptors(actualDescriptor, expectedDescriptor, diffOptions)
}

module.exports = function diffValues(actual, expected) {
const result = concordance.compare(actual, expected, concordanceOptions)
if (result.pass) {
return null
}
const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions)
const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions)

return formatDescriptorDiff(actualDescriptor, expectedDescriptor)
}

0 comments on commit 8adfa6f

Please sign in to comment.