Skip to content
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

Set a SERVICE_API_KEY .env variable on init & use #239

Merged
merged 5 commits into from
May 26, 2020
Merged
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
17 changes: 9 additions & 8 deletions src/commands/app/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ const path = require('path')
const fs = require('fs-extra')
const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:init', { provider: 'debug' })
const { flags } = require('@oclif/command')
const { validateConfig, importConfigJson, loadConfigFile, writeAio } = require('../../lib/import')
const { loadAndValidateConfigFile, importConfigJson, writeAio } = require('../../lib/import')
const { getCliInfo } = require('../../lib/app-helper')
const chalk = require('chalk')

const SERVICE_API_KEY_ENV = 'SERVICE_API_KEY'

class InitCommand extends BaseCommand {
async run () {
const { args, flags } = this.parse(InitCommand)
Expand All @@ -37,6 +39,8 @@ class InitCommand extends BaseCommand {
let projectName = path.basename(process.cwd())
// list of supported service templates
let services = 'AdobeTargetSDK,AdobeAnalyticsSDK,CampaignSDK,McDataServicesSdk,AudienceManagerCustomerSDK'
// client id of the console's workspace jwt credentials
let serviceClientId = ''

if (!(flags.import || flags.yes)) {
try {
Expand All @@ -57,15 +61,12 @@ class InitCommand extends BaseCommand {
}

if (flags.import) {
const { values: config } = loadConfigFile(flags.import)
const { valid: configIsValid, errors: configErrors } = validateConfig(config)
if (!configIsValid) {
const message = `Missing or invalid keys in config: ${JSON.stringify(configErrors, null, 2)}`
this.error(message)
}
const { values: config } = loadAndValidateConfigFile(flags.import)

projectName = config.project.name
services = config.project.workspace.details.services.map(s => s.code).join(',') || ''
const jwtConfig = config.project.workspace.details.credentials && config.project.workspace.details.credentials.find(c => c.jwt)
serviceClientId = (jwtConfig && jwtConfig.jwt.client_id) || serviceClientId // defaults to ''
}

this.log(`You are about to initialize the project '${projectName}'`)
Expand All @@ -84,7 +85,7 @@ class InitCommand extends BaseCommand {
const interactive = false
const merge = true
if (flags.import) {
await importConfigJson(flags.import, process.cwd(), { interactive, merge })
await importConfigJson(flags.import, process.cwd(), { interactive, merge }, { [SERVICE_API_KEY_ENV]: serviceClientId })
} else {
// write default services value to .aio
await writeAio({
Expand Down
23 changes: 16 additions & 7 deletions src/commands/app/use.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ governing permissions and limitations under the License.
*/

const BaseCommand = require('../../BaseCommand')
const { importConfigJson } = require('../../lib/import')
const { importConfigJson, loadAndValidateConfigFile } = require('../../lib/import')
const { flags } = require('@oclif/command')
const inquirer = require('inquirer')
const config = require('@adobe/aio-lib-core-config')
const { EOL } = require('os')
const yeoman = require('yeoman-environment')
const { getCliInfo } = require('../../lib/app-helper')

const SERVICE_API_KEY_ENV = 'SERVICE_API_KEY'

class Use extends BaseCommand {
async consoleConfigString (consoleConfig) {
const { org = {}, project = {}, workspace = {} } = consoleConfig || {}
Expand All @@ -30,7 +32,7 @@ class Use extends BaseCommand {
return { value: list.join(EOL), error }
}

async useConsoleConfig (flags, args) {
async useConsoleConfig () {
const consoleConfig = config.get('$console')
const { value, error } = await this.consoleConfigString(consoleConfig)
if (error) {
Expand Down Expand Up @@ -58,9 +60,9 @@ class Use extends BaseCommand {
'project-id': project.id,
'workspace-id': workspace.id
})

return this.importConfigFile(generatedFile, flags)
return generatedFile
}
return null
}
}

Expand All @@ -73,16 +75,23 @@ class Use extends BaseCommand {
interactive = false
}

return importConfigJson(filePath, process.cwd(), { interactive, overwrite, merge })
const { values: config } = loadAndValidateConfigFile(filePath)
const jwtConfig = config.project.workspace.details.credentials && config.project.workspace.details.credentials.find(c => c.jwt)
const serviceClientId = (jwtConfig && jwtConfig.jwt.client_id) || ''
const extraEnvVars = { [SERVICE_API_KEY_ENV]: serviceClientId }

return importConfigJson(filePath, process.cwd(), { interactive, overwrite, merge }, extraEnvVars)
}

async run () {
const { flags, args } = this.parse(Use)

if (args.config_file_path) {
return this.importConfigFile(args.config_file_path, flags)
} else {
return this.useConsoleConfig(flags)
}
const file = await this.useConsoleConfig(flags)
if (file) {
return this.importConfigFile(file, flags)
}
}
}
Expand Down
43 changes: 29 additions & 14 deletions src/lib/import.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,22 @@ function loadConfigFile (fileOrBuffer) {
return { values: {}, format: 'json' }
}

/**
* Load and validate a config file
*
* @param {string} fileOrBuffer the path to the config file or a Buffer
* @returns {object} object with properties `value` and `format`
*/
function loadAndValidateConfigFile (fileOrBuffer) {
const res = loadConfigFile(fileOrBuffer)
const { valid: configIsValid, errors: configErrors } = validateConfig(res.values)
if (!configIsValid) {
const message = `Missing or invalid keys in config: ${JSON.stringify(configErrors, null, 2)}`
throw new Error(message)
}
return res
}

/**
* Pretty prints the json object as a string.
* Delimited by 2 spaces.
Expand Down Expand Up @@ -339,15 +355,17 @@ async function writeFile (destination, data, flags = {}) {
* @param {boolean} [flags.overwrite=false] set to true to overwrite the existing .env file
* @param {boolean} [flags.merge=false] set to true to merge in the existing .env file (takes precedence over overwrite)
* @param {boolean} [flags.interactive=false] set to true to prompt the user for file overwrite
* @param {object} [extraEnvVars={}] extra environment variables key/value pairs to add to the generated .env.
* Extra variables are treated as raw and won't be rewritten to comply with aio-lib-core-config
* @returns {Promise} promise from writeFile call
*/
async function writeEnv (json, parentFolder, flags) {
aioLogger.debug(`writeEnv - json: ${JSON.stringify(json)} parentFolder:${parentFolder} flags:${flags}`)
async function writeEnv (json, parentFolder, flags, extraEnvVars) {
aioLogger.debug(`writeEnv - json: ${JSON.stringify(json)} parentFolder:${parentFolder} flags:${flags} extraEnvVars:${extraEnvVars}`)

const destination = path.join(parentFolder, ENV_FILE)
aioLogger.debug(`writeEnv - destination: ${destination}`)

const resultObject = flattenObjectWithSeparator(json)
const resultObject = { ...flattenObjectWithSeparator(json), ...extraEnvVars }
aioLogger.debug(`convertJsonToEnv - flattened and separated json: ${prettyPrintJson(resultObject)}`)

const data = Object
Expand Down Expand Up @@ -508,30 +526,26 @@ function transformCredentials (credentials, imsOrgId) {
*
* @param {string} configFileLocation the path to the config file to import
* @param {string} [destinationFolder=the current working directory] the path to the folder to write the .env and .aio files to
* @param {object} [flags] flags for file writing
* @param {object} [flags={}] flags for file writing
* @param {boolean} [flags.overwrite=false] set to true to overwrite the existing .env file
* @param {boolean} [flags.merge=false] set to true to merge in the existing .env file (takes precedence over overwrite)
* @param {object} [extraEnvVars={}] extra environment variables key/value pairs to add to the generated .env.
* Extra variables are treated as raw and won't be rewritten to comply with aio-lib-core-config
* @returns {Promise} promise from writeAio call
*/
async function importConfigJson (configFileLocation, destinationFolder = process.cwd(), flags = {}) {
aioLogger.debug(`importConfigJson - configFileLocation: ${configFileLocation} destinationFolder:${destinationFolder} flags:${flags}`)
async function importConfigJson (configFileLocation, destinationFolder = process.cwd(), flags = {}, extraEnvVars = {}) {
aioLogger.debug(`importConfigJson - configFileLocation: ${configFileLocation} destinationFolder:${destinationFolder} flags:${flags} extraEnvVars:${extraEnvVars}`)

const { values: config, format } = loadConfigFile(configFileLocation)
const { valid: configIsValid, errors: configErrors } = validateConfig(config)
const { values: config, format } = loadAndValidateConfigFile(configFileLocation)

aioLogger.debug(`importConfigJson - format: ${format} config:${prettyPrintJson(config)} `)

if (!configIsValid) {
const message = `Missing or invalid keys in config: ${JSON.stringify(configErrors, null, 2)}`
throw new Error(message)
}

const { runtime, credentials } = config.project.workspace.details

await writeEnv({
runtime: transformRuntime(runtime),
$ims: transformCredentials(credentials, config.project.org.ims_org_id)
}, destinationFolder, flags)
}, destinationFolder, flags, extraEnvVars)

// remove the credentials
delete config.project.workspace.details.runtime
Expand All @@ -546,6 +560,7 @@ async function importConfigJson (configFileLocation, destinationFolder = process
module.exports = {
validateConfig,
loadConfigFile,
loadAndValidateConfigFile,
writeConsoleConfig,
writeAio,
writeEnv,
Expand Down
97 changes: 72 additions & 25 deletions test/commands/app/init.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,46 +102,43 @@ const fullServicesJson = [
{ code: 'AudienceManagerCustomerSDK' }
]

const fakeCredentials = [
{
id: '1',
fake: { client_id: 'notjwtId' }
},
{
id: '2',
jwt: { client_id: 'fakeId123' }
}
]
/** @private */
function getFullServicesList () {
return fullServicesJson.map(s => s.code).join(',')
}

/** @private */
function mockValidConfig ({ name = 'lifeisgood', services = fullServicesJson } = {}) {
function mockValidConfig ({ name = 'lifeisgood', services = fullServicesJson, credentials = fakeCredentials } = {}) {
const project = {
name,
workspace: {
details: {
services
services,
credentials
}
}
}

importLib.loadConfigFile.mockReturnValue({
importLib.loadAndValidateConfigFile.mockReturnValue({
values: { project }
})
importLib.validateConfig.mockReturnValue({
valid: true
})

return project
}

/** @private */
function mockInvalidConfig () {
const foo = {
bar: 'lifeismeh'
}

importLib.loadConfigFile.mockReturnValue({
values: { foo }
})
importLib.validateConfig.mockReturnValue({
valid: false
})

return foo
importLib.loadAndValidateConfigFile.mockImplementation(() => { throw new Error('fake error') })
}

describe('run', () => {
Expand Down Expand Up @@ -335,10 +332,10 @@ describe('run', () => {

test('no-path --import file=invalid config', async () => {
mockInvalidConfig()
await expect(TheCommand.run(['--import', 'config.json'])).rejects.toThrow('Missing or invalid keys in config:')
await expect(TheCommand.run(['--import', 'config.json'])).rejects.toThrow('fake error')
})

test('no-path --import file={name: lifeisgood, services:AdobeTargetSDK,CampaignSDK}', async () => {
test('no-path --import file={name: lifeisgood, services:AdobeTargetSDK,CampaignSDK, credentials:fake,jwt}', async () => {
const project = mockValidConfig({
name: 'lifeisgood',
services: [{ code: 'AdobeTargetSDK' }, { code: 'CampaignSDK' }]
Expand All @@ -359,10 +356,60 @@ describe('run', () => {
'adobe-services': 'AdobeTargetSDK,CampaignSDK'
})

expect(importLib.importConfigJson).toHaveBeenCalledWith('config.json', process.cwd(), { interactive: false, merge: true })
expect(importLib.importConfigJson).toHaveBeenCalledWith('config.json', process.cwd(), { interactive: false, merge: true }, { SERVICE_API_KEY: 'fakeId123' })
})

test('no-path --import file={name: lifeisgood, services:AdobeTargetSDK,CampaignSDK, credentials:fake}', async () => {
const project = mockValidConfig({
name: 'lifeisgood',
services: [{ code: 'AdobeTargetSDK' }, { code: 'CampaignSDK' }],
credentials: [{ id: '1', fake: { client_id: 'notjwtId' } }]
})
await TheCommand.run(['--import', 'config.json'])

// no args.path
expect(fs.ensureDirSync).not.toHaveBeenCalled()
expect(spyChdir).not.toHaveBeenCalled()

expect(yeoman.createEnv).toHaveBeenCalled()
expect(mockRegister).toHaveBeenCalledTimes(1)
const genApp = mockRegister.mock.calls[0][1]
expect(mockRun).toHaveBeenNthCalledWith(1, genApp, {
'skip-prompt': false,
'skip-install': false,
'project-name': project.name,
'adobe-services': 'AdobeTargetSDK,CampaignSDK'
})

expect(importLib.importConfigJson).toHaveBeenCalledWith('config.json', process.cwd(), { interactive: false, merge: true }, { SERVICE_API_KEY: '' })
})

test('no-path --import file={name: lifeisgood, services:AdobeTargetSDK,CampaignSDK, credentials:null}', async () => {
const project = mockValidConfig({
name: 'lifeisgood',
services: [{ code: 'AdobeTargetSDK' }, { code: 'CampaignSDK' }],
credentials: null
})
await TheCommand.run(['--import', 'config.json'])

// no args.path
expect(fs.ensureDirSync).not.toHaveBeenCalled()
expect(spyChdir).not.toHaveBeenCalled()

expect(yeoman.createEnv).toHaveBeenCalled()
expect(mockRegister).toHaveBeenCalledTimes(1)
const genApp = mockRegister.mock.calls[0][1]
expect(mockRun).toHaveBeenNthCalledWith(1, genApp, {
'skip-prompt': false,
'skip-install': false,
'project-name': project.name,
'adobe-services': 'AdobeTargetSDK,CampaignSDK'
})

expect(importLib.importConfigJson).toHaveBeenCalledWith('config.json', process.cwd(), { interactive: false, merge: true }, { SERVICE_API_KEY: '' })
})

test('no-path --yes --import file={name: lifeisgood, services:AdobeTargetSDK,CampaignSDK}', async () => {
test('no-path --yes --import file={name: lifeisgood, services:AdobeTargetSDK,CampaignSDK, credentials:fake,jwt}', async () => {
const project = mockValidConfig({
name: 'lifeisgood',
services: [{ code: 'AdobeTargetSDK' }, { code: 'CampaignSDK' }]
Expand All @@ -383,10 +430,10 @@ describe('run', () => {
'adobe-services': 'AdobeTargetSDK,CampaignSDK'
})

expect(importLib.importConfigJson).toHaveBeenCalledWith('config.json', process.cwd(), { interactive: false, merge: true })
expect(importLib.importConfigJson).toHaveBeenCalledWith('config.json', process.cwd(), { interactive: false, merge: true }, { SERVICE_API_KEY: 'fakeId123' })
})

test('some-path --import file={name: lifeisgood, services:undefined}', async () => {
test('some-path --import file={name: lifeisgood, services:undefined, credentials:fake,jwt}', async () => {
const project = mockValidConfig({ name: 'lifeisgood', services: [] })
await TheCommand.run(['some-path', '--import', 'config.json'])

Expand All @@ -404,7 +451,7 @@ describe('run', () => {
'adobe-services': ''
})

expect(importLib.importConfigJson).toHaveBeenCalledWith('config.json', process.cwd(), { interactive: false, merge: true })
expect(importLib.importConfigJson).toHaveBeenCalledWith('config.json', process.cwd(), { interactive: false, merge: true }, { SERVICE_API_KEY: 'fakeId123' })
})

test('no cli context', async () => {
Expand Down
Loading