Skip to content

Commit

Permalink
Add AWS profile parameter support (#171)
Browse files Browse the repository at this point in the history
* Add AWS profile parameter support

* Changeset

* Fix package.json & lock

* Fix formatting

* Test ci command

* Update action versions
  • Loading branch information
andries-miro authored Sep 20, 2024
1 parent 7f90266 commit 01223ed
Show file tree
Hide file tree
Showing 16 changed files with 4,806 additions and 10,541 deletions.
5 changes: 5 additions & 0 deletions .changeset/tall-suits-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mirohq/cloud-data-import': minor
---

Add AWS profile parameter support
6 changes: 3 additions & 3 deletions .github/workflows/ci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ jobs:

steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install Dependencies
run: npm install
run: npm ci

- name: Run Linter
run: npm run lint
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ jobs:

steps:
- name: Checkout Code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install Dependencies
run: npm install
run: npm ci

- name: Run Tests
run: npm test
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm install
run: npm ci

- name: Build package
run: npm run build
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The script accepts several arguments to customize the data import process:
| Argument | Description |
| -------------------------- | ---------------------------------------------------------------------------------------------- |
| `-r, --regions` | Specify the AWS regions to scan. Use `"all"` to scan all available regions. |
| `-p, --profile` | Specify the AWS profile to use (takes priority over the AWS_PROFILE environment variable). |
| `-o, --output` | Define the output file path for the imported data. Must be a `.json` file. |
| `--rps, --call-rate-rps` | Set the maximum number of API calls to make per second. Default is 10. |
| `-c, --compressed` | Enable output compression. |
Expand Down
15,094 changes: 4,576 additions & 10,518 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"@aws-sdk/client-sns": "^3.621.0",
"@aws-sdk/client-sqs": "^3.621.0",
"@aws-sdk/client-sts": "^3.654.0",
"@aws-sdk/credential-providers": "3.654.0",
"@aws-sdk/smithy-client": "^3.374.0",
"@aws-sdk/util-arn-parser": "^3.568.0",
"@mirohq/prettier-config": "^2.0.0",
Expand Down
6 changes: 6 additions & 0 deletions src/aws-app/config/getConfigFromProgramArguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export const getConfigFromProgramArguments = (): Config => {
return arg
},
})
.option('profile', {
alias: 'p',
type: 'string',
description: 'Specify the AWS profile to use (takes priority over the AWS_PROFILE environment variable).',
default: getEnvConfig(SUPPORTED_ENV_VARS.PROFILE) || process.env.AWS_PROFILE || 'default',
})
.option('output', {
alias: 'o',
type: 'string',
Expand Down
55 changes: 45 additions & 10 deletions src/aws-app/config/getConfigViaGui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ const regionPrompt = async (defaultRegions?: string[]): Promise<string[]> => {
return regions.includes('all') ? awsRegionIds : regions
}

const profilePrompt = async (defaultProfile?: string): Promise<string> => {
const {profile} = await inquirer.prompt([
{
type: 'input',
name: 'profile',
message: 'AWS profile:',
default: defaultProfile,
},
])
return profile
}

const outputPathPrompt = async (defaultPath: string): Promise<string> => {
const {output} = await inquirer.prompt([
{
Expand Down Expand Up @@ -67,21 +79,44 @@ const scanGlobalPrompt = async (defaultScanGlobal: boolean): Promise<boolean> =>
return scanGlobal
}

export const getDefaultConfigValues = (): {
output: string
regions: any
profile: string
regionalOnly: boolean
callRate: number
compressed: boolean
} => {
const regions = getEnvConfig(SUPPORTED_ENV_VARS.REGIONS)?.split(',')
const profile = getEnvConfig(SUPPORTED_ENV_VARS.PROFILE) || process.env.AWS_PROFILE || 'default'
const regionalOnly = getEnvConfig(SUPPORTED_ENV_VARS.REGIONAL_ONLY) === 'true'
const output = getEnvConfig(SUPPORTED_ENV_VARS.OUTPUT) || getDefaultOutputName()
const callRate = parseInt(getEnvConfig(SUPPORTED_ENV_VARS.CALL_RATE_RPS) || '') || 10
const compressed = getEnvConfig(SUPPORTED_ENV_VARS.COMPRESSED) === 'true'

return {
regions,
profile,
regionalOnly,
output,
callRate,
compressed,
}
}

export const getConfigViaGui = async (): Promise<Config> => {
const defaultRegions = getEnvConfig(SUPPORTED_ENV_VARS.REGIONS)?.split(',')
const defaultRegionalOnly = getEnvConfig(SUPPORTED_ENV_VARS.REGIONAL_ONLY) === 'true'
const defaultOutput = getEnvConfig(SUPPORTED_ENV_VARS.OUTPUT) || getDefaultOutputName()
const defaultCallRate = parseInt(getEnvConfig(SUPPORTED_ENV_VARS.CALL_RATE_RPS) || '') || 10
const defaultCompressed = getEnvConfig(SUPPORTED_ENV_VARS.COMPRESSED) === 'true'
const defaults = getDefaultConfigValues()

const regions = await regionPrompt(defaultRegions)
const scanGlobal = await scanGlobalPrompt(!defaultRegionalOnly)
const output = await outputPathPrompt(defaultOutput)
const callRateRps = await callRatePrompt(defaultCallRate)
const compressed = await compressedPrompt(defaultCompressed)
const regions = await regionPrompt(defaults.regions)
const profile = await profilePrompt(defaults.profile)
const scanGlobal = await scanGlobalPrompt(!defaults.regionalOnly)
const output = await outputPathPrompt(defaults.output)
const callRateRps = await callRatePrompt(defaults.callRate)
const compressed = await compressedPrompt(defaults.compressed)

return {
regions,
profile,
output,
'call-rate-rps': callRateRps,
compressed,
Expand Down
1 change: 1 addition & 0 deletions src/aws-app/config/getEnvConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const ENV_VAR_PREFIX = 'CLOUDVIEW_AWS_'

export enum SUPPORTED_ENV_VARS {
REGIONS = 'REGIONS',
PROFILE = 'PROFILE',
OUTPUT = 'OUTPUT',
CALL_RATE_RPS = 'CALL_RATE_RPS',
COMPRESSED = 'COMPRESSED',
Expand Down
3 changes: 2 additions & 1 deletion src/aws-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {getProcessedData} from './process'
import {getConfig} from './config'
import {createRateLimiterFactory} from './utils/createRateLimiterFactory'
import {getAwsAccountId} from '@/scanners/scan-functions/aws/common/getAwsAccountId'
import {buildCredentialIdentity} from '@/aws-app/utils/buildCredentialIdentity'

export default async () => {
console.log(cliMessages.getIntro())
Expand All @@ -20,7 +21,7 @@ export default async () => {

const getRateLimiter = createRateLimiterFactory(config['call-rate-rps'])

const credentials = undefined // assume that the credentials are already set in the environment
const credentials = await buildCredentialIdentity(config.profile)

// prepare scanners
const scanners = getAwsScanners({
Expand Down
24 changes: 24 additions & 0 deletions src/aws-app/utils/buildCredentialIdentity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {AwsCredentialIdentity} from '@aws-sdk/types'
import {fromIni} from '@aws-sdk/credential-providers'

/**
* Builds the AwsCredentialIdentity
*
* it is important to note that if there is more than one credential source available to the SDK, a default precedence of selection will be followed.
* see: https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html
*/
export const buildCredentialIdentity = async (profile: string): Promise<AwsCredentialIdentity> => {
const credentialsProvider = fromIni({
profile: profile,
})

let credentialIdentity: AwsCredentialIdentity
try {
credentialIdentity = await credentialsProvider()
} catch (error) {
console.error(`\n[ERROR] Failed to resolve AWS credentials\n`)
throw error
}

return credentialIdentity
}
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export type Scanner<T extends ResourceDescription = ResourceDescription> = () =>

export interface Config {
regions: string[]
profile: string
output: string
compressed: boolean
raw: boolean
Expand Down
61 changes: 61 additions & 0 deletions tests/aws-app/config/getConfigViaGui.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {getDefaultConfigValues} from '@/aws-app/config/getConfigViaGui'
import {getDefaultOutputName} from '@/aws-app/config/getDefaultOutputName'

jest.mock('@/aws-app/config/getDefaultOutputName')

describe('getConfigViaGui', () => {
const defaultConfigResponse = {
output: 'dummyOutputName',
regions: undefined,
profile: 'default',
regionalOnly: false,
callRate: 10,
compressed: false,
}

const originalEnv = process.env

beforeEach(() => {
;(getDefaultOutputName as jest.Mock).mockReturnValue('dummyOutputName')

jest.clearAllMocks()

process.env = {
...originalEnv,
}
})

afterEach(() => {
jest.restoreAllMocks()
jest.resetAllMocks()
})

it('should return correct default values', async () => {
const defaultConfig = await getDefaultConfigValues()

expect(defaultConfig).toStrictEqual(defaultConfigResponse)
})

it('should return correct prioritize the CLOUDVIEW_AWS_PROFILE environment variable over the AWS_PROFILE environment variable', async () => {
process.env.CLOUDVIEW_AWS_PROFILE = 'dummyCloudviewAwsProfileEnvVar'
process.env.AWS_PROFILE = 'dummyAwsProfileEnvVar'

const defaultConfig = await getDefaultConfigValues()

expect(defaultConfig).toStrictEqual({
...defaultConfigResponse,
profile: 'dummyCloudviewAwsProfileEnvVar',
})
})

it('should fall back to the AWS_PROFILE environment variable when CLOUDVIEW_AWS_PROFILE is not configured', async () => {
process.env.AWS_PROFILE = 'dummyAwsProfileEnvVar'

const defaultConfig = await getDefaultConfigValues()

expect(defaultConfig).toStrictEqual({
...defaultConfigResponse,
profile: 'dummyAwsProfileEnvVar',
})
})
})
27 changes: 24 additions & 3 deletions tests/aws-app/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import {getProcessedData} from '@/aws-app/process'
import {getConfig} from '@/aws-app/config'
import {createRateLimiterFactory} from '@/aws-app/utils/createRateLimiterFactory'
import {getAwsAccountId} from '@/scanners/scan-functions/aws/common/getAwsAccountId'
import {buildCredentialIdentity} from '@/aws-app/utils/buildCredentialIdentity'
import {AwsCredentialIdentity} from '@aws-sdk/types'

import {mockDate} from '../mocks/dateMock'

jest.mock('@/aws-app/hooks/Logger')
jest.mock('@/scanners')
jest.mock('@/aws-app/utils/saveAsJson')
jest.mock('@/aws-app/utils/buildCredentialIdentity')
jest.mock('@/aws-app/cliMessages')
jest.mock('@/aws-app/utils/openDirectoryAndFocusFile')
jest.mock('@/aws-app/process')
Expand All @@ -31,6 +34,12 @@ describe('main function', () => {
let config: any
let mockedDate: ReturnType<typeof mockDate>

const mockCredentials: AwsCredentialIdentity = {
accessKeyId: 'mockAccessKeyId',
secretAccessKey: 'mockSecretAccessKey',
sessionToken: 'mockSessionToken',
}

const mockedProcessedData: ProcessedData = {
resources: {
'dummy:arn:1': {
Expand Down Expand Up @@ -85,9 +94,10 @@ describe('main function', () => {
getOutroSpy = jest.spyOn(cliMessages, 'getOutro').mockReturnValue('Outro message')

mockedDate = mockDate(15000) // 15 seconds between Date.now() calls

;(buildCredentialIdentity as jest.Mock).mockResolvedValue(mockCredentials)
config = {
regions: ['us-east-1', 'eu-west-1'],
profile: 'default',
output: 'output.json',
compressed: false,
raw: true,
Expand Down Expand Up @@ -136,20 +146,31 @@ describe('main function', () => {
const main = (await import('@/aws-app/main')).default
await main()
expect(getAwsScanners).toHaveBeenCalledWith({
credentials: undefined,
credentials: mockCredentials,
regions: config.regions,
getRateLimiter: expect.any(Function),
shouldIncludeGlobalServices: true,
hooks: [expect.any(Logger)],
})
})

it('should call getAwsScanners with the correct AWS profile', async () => {
const dummyConfig = {
...config,
profile: 'dummyProfile',
}
;(getConfig as jest.Mock).mockResolvedValue(dummyConfig)
const main = (await import('@/aws-app/main')).default
await main()
expect(buildCredentialIdentity as jest.Mock).toHaveBeenCalledWith('dummyProfile')
})

it('should aggregate resources and errors correctly', async () => {
const main = (await import('@/aws-app/main')).default

await main()

const dateCallsBeforeMeasure = 4 // @todo implement a more robust way to mock Date
const dateCallsBeforeMeasure = 6 // @todo implement a more robust way to mock Date

const expectedStartedAt = mockedDate.getExpectedTimeISOString(dateCallsBeforeMeasure) // first Date.now() call
const expectedFinishedAt = mockedDate.getExpectedTimeISOString(dateCallsBeforeMeasure + 1) // second Date.now() call
Expand Down
Loading

0 comments on commit 01223ed

Please sign in to comment.