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

Add AWS profile parameter support #171

Merged
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
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
Loading