Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
66 changes: 66 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/sf-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"tape-promise": "^4.0.0"
},
"dependencies": {
"@aws-sdk/client-appconfigdata": "3.1017.0",
"@aws-sdk/client-cloudformation": "3.1017.0",
"@aws-sdk/client-s3": "3.1017.0",
"@aws-sdk/client-ssm": "3.1017.0",
Expand Down
101 changes: 101 additions & 0 deletions packages/sf-core/src/lib/resolvers/providers/aws/appconfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
AppConfigDataClient,
StartConfigurationSessionCommand,
GetLatestConfigurationCommand,
} from '@aws-sdk/client-appconfigdata'
import { addProxyToAwsClient } from '@serverless/util'

export const resolveValueFromAppConfig = async (
credentials,
region,
key,
providerConfig,
) => {
/**
* The key can be in the format:
* ${aws:appconfig:applicationId/environmentId/configurationProfileId}
* ${aws:appconfig:applicationId/environmentId/configurationProfileId/jsonPath}
*
* Or if applicationId, environmentId, and configurationProfileId are
* in the provider config, the key is used as a JSON path into
* the configuration data.
*/
let applicationId = providerConfig.applicationId
let environmentId = providerConfig.environmentId
let configurationProfileId = providerConfig.configurationProfileId
let jsonPath = null

const parts = key.split('/')
if (parts.length >= 3) {
applicationId = parts[0]
environmentId = parts[1]
configurationProfileId = parts[2]
if (parts.length > 3) {
jsonPath = parts.slice(3).join('/')
}
} else if (!applicationId || !environmentId || !configurationProfileId) {
throw new Error(
'AWS AppConfig requires applicationId, environmentId, and configurationProfileId. Provide them in the resolver config or as "${aws:appconfig:appId/envId/profileId}"',
)
} else {
// All IDs from config, key is a JSON path
jsonPath = key
}

const client = addProxyToAwsClient(
new AppConfigDataClient({
credentials,
region,
}),
)

// Start a configuration session
const sessionCommand = new StartConfigurationSessionCommand({
ApplicationIdentifier: applicationId,
EnvironmentIdentifier: environmentId,
ConfigurationProfileIdentifier: configurationProfileId,
})
const session = await client.send(sessionCommand)

// Get the latest configuration
const configCommand = new GetLatestConfigurationCommand({
ConfigurationToken: session.InitialConfigurationToken,
})
const configResult = await client.send(configCommand)

// Convert the configuration blob to string
const configString = new TextDecoder().decode(configResult.Configuration)

if (!configString) {
throw new Error('No configuration data returned from AWS AppConfig')
}

// If jsonPath is provided, parse the config as JSON and extract the value
if (jsonPath) {
let configData
try {
configData = JSON.parse(configString)
} catch {
// If JSON parsing fails, return raw config string
return configString
}
const keys = jsonPath.split('.')
let value = configData
for (const k of keys) {
if (value === undefined || value === null) {
throw new Error(
`Key "${jsonPath}" not found in AWS AppConfig configuration`,
)
}
value = value[k]
}
if (value === undefined) {
throw new Error(
`Key "${jsonPath}" not found in AWS AppConfig configuration`,
)
}
return typeof value === 'string' ? value : JSON.stringify(value)
}

return configString
}
11 changes: 10 additions & 1 deletion packages/sf-core/src/lib/resolvers/providers/aws/aws.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { AbstractProvider } from '../index.js'
import { resolveVariableFromSsm } from './ssm.js'
import { resolveVariableFromS3, storeDataInS3 } from './s3.js'
import { resolveVariableFromCloudFormation } from './cf.js'
import { resolveValueFromAppConfig } from './appconfig.js'
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'
import { addProxyToAwsClient } from '@serverless/util'
import { getAwsCredentials } from './credentials.js'
import { ServerlessError, ServerlessErrorCodes } from '@serverless/util'

export class Aws extends AbstractProvider {
static type = 'aws'
static resolvers = ['ssm', 's3', 'cf']
static resolvers = ['ssm', 's3', 'cf', 'appconfig']
static defaultResolver = 'ssm'

static validateConfig(config) {
Expand Down Expand Up @@ -125,6 +126,14 @@ export class Aws extends AbstractProvider {
key,
)
}
if (resolverType === 'appconfig') {
return resolveValueFromAppConfig(
this.credentials,
region,
key,
this.config,
)
}
} catch (error) {
let err
if (error.name === 'ExpiredToken') {
Expand Down
Loading
Loading