Skip to content

Commit

Permalink
Set a SERVICE_API_KEY .env variable on init & use (#239)
Browse files Browse the repository at this point in the history
* [WIP] set a SERVICE_API_KEY env variable on init & use

* missing debug var

* missing await

* add tests and config validation for use config
  • Loading branch information
moritzraho authored May 26, 2020
1 parent bb398aa commit 5406d4f
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 54 deletions.
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

0 comments on commit 5406d4f

Please sign in to comment.