diff --git a/provider/scalewayProvider.js b/provider/scalewayProvider.js index 3b2fe887..802c76c7 100644 --- a/provider/scalewayProvider.js +++ b/provider/scalewayProvider.js @@ -1,5 +1,10 @@ 'use strict'; +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const yaml = require('js-yaml'); + const BbPromise = require('bluebird'); const { FUNCTIONS_API_URL } = require('../shared/constants'); const { CONTAINERS_API_URL } = require('../shared/constants'); @@ -9,6 +14,8 @@ const { DEFAULT_REGION } = require('../shared/constants'); const providerName = 'scaleway'; class ScalewayProvider { + static scwConfigFile = path.join(os.homedir(), ".config", "scw", "config.yaml"); + static getProviderName() { return providerName; } @@ -39,37 +46,67 @@ class ScalewayProvider { setCredentials(options) { // On serverless info command we do not want log pollution from authentication. - // This is necessary to use it in automated environment. + // This is necessary to use it in an automated environment. let hideLog = false; - if (this.serverless.configurationInput.service && this.serverless.configurationInput.service === 'serverlessInfo') { + if (this.serverless.configurationInput.service && + this.serverless.configurationInput.service === 'serverlessInfo') { hideLog = true; } + if (options['scw-token'] && options['scw-project']) { if (!hideLog) { this.serverless.cli.log('Using credentials from command line parameters'); } + this.scwToken = options['scw-token']; this.scwProject = options['scw-project']; + } else if (process.env.SCW_SECRET_KEY && process.env.SCW_DEFAULT_PROJECT_ID) { if (!hideLog) { this.serverless.cli.log('Using credentials from system environment'); } + this.scwToken = process.env.SCW_SECRET_KEY; this.scwProject = process.env.SCW_DEFAULT_PROJECT_ID; + } else if (process.env.SCW_TOKEN && process.env.SCW_PROJECT) { if (!hideLog) { this.serverless.cli.log('Using credentials from system environment'); this.serverless.cli.log('NOTICE: you are using deprecated environment variable notation,'); this.serverless.cli.log('please update to SCW_SECRET_KEY and SCW_DEFAULT_PROJECT_ID'); } + this.scwToken = process.env.SCW_TOKEN; this.scwProject = process.env.SCW_PROJECT; + + } else if (this.serverless.service.provider.scwToken || + this.serverless.service.provider.scwProject) { + if (!hideLog) { + this.serverless.cli.log('Using credentials from serverless.yml'); + } + + this.scwToken = this.serverless.service.provider.scwToken; + this.scwProject = this.serverless.service.provider.scwProject; + + } else if (fs.existsSync(ScalewayProvider.scwConfigFile)) { + if (!hideLog) { + this.serverless.cli.log(`Using credentials from ${ScalewayProvider.scwConfigFile}`); + } + + let fileData = fs.readFileSync(ScalewayProvider.scwConfigFile, 'utf8'); + let scwConfig = yaml.load(fileData); + + this.scwToken = scwConfig.secret_key; + this.scwProject = scwConfig.default_project_id; + this.scwRegion = scwConfig.default_region; + } else { if (!hideLog) { - this.serverless.cli.log('Using credentials from yml'); + this.serverless.cli.log('Unable to locate Scaleway provider credentials'); } - this.scwToken = this.serverless.service.provider.scwToken || ''; - this.scwProject = this.serverless.service.provider.scwProject || ''; + + this.scwToken = ''; + this.scwProject = ''; } } diff --git a/tests/provider/scalewayProvider.test.js b/tests/provider/scalewayProvider.test.js new file mode 100644 index 00000000..af87349f --- /dev/null +++ b/tests/provider/scalewayProvider.test.js @@ -0,0 +1,137 @@ +const { expect } = require('chai'); +const { expect: jestExpect } = require('@jest/globals'); + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const ScalewayProvider = require('../../provider/scalewayProvider'); +const { createTmpDir } = require('../utils/fs'); + +class MockServerless { + constructor() { + this.service = {}; + this.service.provider = {}; + + this.cli = {}; + this.cli.log = (logMsg) => { + console.log(logMsg); + } + }; + + setProvider(provName, prov) { + this.service.provider = prov; + }; +}; + +describe('Scaleway credentials test', () => { + this.expectedToken = null; + this.expectedProject = null; + + this.serverless = new MockServerless(); + this.prov = new ScalewayProvider(this.serverless); + + beforeAll(() => { + // Override scw config file location + this.dummyScwConfigDir = createTmpDir(); + this.dummyScwConfigPath = path.join(this.dummyScwConfigDir, 'config.yml'); + + ScalewayProvider.scwConfigFile = this.dummyScwConfigPath; + }); + + afterAll(() => { + // Delete the dummy config file and directory + if(fs.existsSync(this.dummyScwConfigPath)) { + fs.unlinkSync(this.dummyScwConfigPath); + } + + if(fs.existsSync(this.dummyScwConfigDir)) { + fs.rmdirSync(this.dummyScwConfigDir); + } + }); + + this.checkCreds = (options) => { + // Set the credentials + this.prov.setCredentials(options); + + // Check they're as expected + expect(this.prov.scwToken).to.equal(this.expectedToken); + expect(this.prov.scwProject).to.equal(this.expectedProject); + }; + + // ------------------------------------- + // These tests must be written in order of increasing precedence, each one getting superceded by the next. + // ------------------------------------- + + it('should return nothing when no credentials found', () => { + this.expectedToken = ''; + this.expectedProject = ''; + + this.checkCreds({}); + }); + + it('should read from scw config file if present', () => { + // Write the dummy file + const dummyScwConfigContents = 'secret_key: scw-key\ndefault_project_id: scw-proj\n'; + fs.writeFileSync(this.dummyScwConfigPath, dummyScwConfigContents); + + this.expectedToken = 'scw-key'; + this.expectedProject = 'scw-proj'; + + this.checkCreds({}); + }); + + it('should take values from serverless.yml if present', () => { + this.expectedToken = 'conf-token'; + this.expectedProject = 'conf-proj'; + + this.serverless.service.provider.scwToken = this.expectedToken; + this.serverless.service.provider.scwProject = this.expectedProject; + + this.checkCreds({}); + }); + + it('should read from legacy environment variables if present', () => { + let originalToken = process.env.SCW_TOKEN; + let originalProject = process.env.SCW_PROJECT; + + this.expectedToken = 'legacy-token'; + this.expectedProject = 'legacy-proj'; + + process.env.SCW_TOKEN = this.expectedToken; + process.env.SCW_PROJECT = this.expectedProject; + + this.checkCreds({}); + + process.env.SCW_TOKEN = originalToken; + process.env.SCW_PROJECT = originalProject; + }); + + it('should read from environment variables if present', () => { + let originalToken = process.env.SCW_SECRET_KEY; + let originalProject = process.env.SCW_DEFAULT_PROJECT_ID; + + this.expectedToken = 'env-token'; + this.expectedProject = 'env-proj'; + + process.env.SCW_SECRET_KEY = this.expectedToken; + process.env.SCW_DEFAULT_PROJECT_ID = this.expectedProject; + + this.checkCreds({}); + + process.env.SCW_SECRET_KEY = originalToken; + process.env.SCW_DEFAULT_PROJECT_ID = originalProject; + }); + + it('should read credentials from options if present', () => { + let options = {}; + options['scw-token'] = 'opt-token'; + options['scw-project'] = 'opt-proj'; + + this.expectedToken = 'opt-token'; + this.expectedProject = 'opt-proj'; + + this.checkCreds(options); + }); +}); + diff --git a/tests/utils/fs/index.js b/tests/utils/fs/index.js index 676a96a8..16a778e8 100644 --- a/tests/utils/fs/index.js +++ b/tests/utils/fs/index.js @@ -16,6 +16,12 @@ function getTmpDirPath() { return path.join(tmpDirCommonPath, crypto.randomBytes(8).toString('hex')); } +function createTmpDir() { + const tmpDir = getTmpDirPath(); + fs.mkdirSync(tmpDir, { recursive: true }); + return tmpDir; +} + function replaceTextInFile(filePath, subString, newSubString) { const fileContent = fs.readFileSync(filePath).toString(); fs.writeFileSync(filePath, fileContent.replace(subString, newSubString)); @@ -35,6 +41,7 @@ function writeYamlFile(filePath, content) { module.exports = { tmpDirCommonPath, getTmpDirPath, + createTmpDir, replaceTextInFile, readYamlFile,