techdocs: add support for integrations.awsS3

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2025-12-03 23:17:31 +01:00
parent e85bd02aeb
commit 4fb08a1b31
2 changed files with 241 additions and 21 deletions
@@ -270,25 +270,177 @@ describe('AwsS3Publish', () => {
expect(getCredProviderMock).toHaveBeenCalledTimes(1);
});
it('should fall back to deprecated method of retrieving credentials', async () => {
it('should use awsS3.credentials if they are provided', async () => {
await jest.isolateModulesAsync(async () => {
jest.doMock('@aws-sdk/client-s3', () => ({
...jest.requireActual('@aws-sdk/client-s3'),
S3Client: jest.fn(),
}));
const { S3Client: MockS3Client } = require('@aws-sdk/client-s3');
const { AwsS3Publish: IsolatedAwsS3Publish } = require('./awsS3');
const mockConfig = new ConfigReader({
techdocs: {
publisher: {
type: 'awsS3',
awsS3: {
credentials: {
accessKeyId: 'accessKeyId',
secretAccessKey: 'secretAccessKey',
},
bucketName: 'bucketName',
bucketRootPath: '/',
},
},
},
integrations: {
awsS3: [
{
accessKeyId: 'access-key-from-integrations',
secretAccessKey: 'secret-access-key-from-integrations',
},
],
},
});
await IsolatedAwsS3Publish.fromConfig(mockConfig, logger);
expect(getCredProviderMock).toHaveBeenCalledTimes(0);
expect(MockS3Client).toHaveBeenCalledTimes(1);
await expect(
MockS3Client.mock.calls[0][0]!.credentialDefaultProvider!(
undefined!,
)(),
).resolves.toEqual({
accessKeyId: 'accessKeyId',
secretAccessKey: 'secretAccessKey',
});
});
});
it('should use credentials from integrations if awsS3.credentials is not provided', async () => {
await jest.isolateModulesAsync(async () => {
jest.doMock('@aws-sdk/client-s3', () => ({
...jest.requireActual('@aws-sdk/client-s3'),
S3Client: jest.fn(),
}));
const { S3Client: MockS3Client } = require('@aws-sdk/client-s3');
const { AwsS3Publish: IsolatedAwsS3Publish } = require('./awsS3');
const mockConfig = new ConfigReader({
techdocs: {
publisher: {
type: 'awsS3',
awsS3: {
credentials: {},
bucketName: 'bucketName',
bucketRootPath: '/',
},
},
},
integrations: {
awsS3: [
{
accessKeyId: 'access-key-from-integrations',
secretAccessKey: 'secret-access-key-from-integrations',
},
],
},
});
await IsolatedAwsS3Publish.fromConfig(mockConfig, logger);
expect(getCredProviderMock).toHaveBeenCalledTimes(0);
expect(MockS3Client).toHaveBeenCalledTimes(1);
await expect(
MockS3Client.mock.calls[0][0]!.credentialDefaultProvider!(
undefined!,
)(),
).resolves.toEqual({
accessKeyId: 'access-key-from-integrations',
secretAccessKey: 'secret-access-key-from-integrations',
});
});
});
it('should retrieve default credentials if multiple integrations are present', async () => {
const mockConfig = new ConfigReader({
techdocs: {
publisher: {
type: 'awsS3',
awsS3: {
credentials: {
accessKeyId: 'accessKeyId',
secretAccessKey: 'secretAccessKey',
},
credentials: {},
bucketName: 'bucketName',
bucketRootPath: '/',
},
},
},
integrations: {
awsS3: [
{
accessKeyId: 'access-key-from-integrations',
secretAccessKey: 'secret-access-key-from-integrations',
},
{
accessKeyId: 'access-key-from-integrations-2',
secretAccessKey: 'secret-access-key-from-integrations-2',
},
],
},
});
await AwsS3Publish.fromConfig(mockConfig, logger);
expect(getCredProviderMock).toHaveBeenCalledTimes(0);
expect(getCredProviderMock).toHaveBeenCalledTimes(1);
});
it('should retrieve the target integration if multiple integrations are provided and credentials are not provided', async () => {
await jest.isolateModulesAsync(async () => {
jest.doMock('@aws-sdk/client-s3', () => ({
...jest.requireActual('@aws-sdk/client-s3'),
S3Client: jest.fn(),
}));
const { S3Client: MockS3Client } = require('@aws-sdk/client-s3');
const { AwsS3Publish: IsolatedAwsS3Publish } = require('./awsS3');
const mockConfig = new ConfigReader({
techdocs: {
publisher: {
type: 'awsS3',
awsS3: {
credentials: {
accessKeyId: 'access-key-from-integrations-2',
},
bucketName: 'bucketName',
bucketRootPath: '/',
},
},
},
integrations: {
awsS3: [
{
accessKeyId: 'access-key-from-integrations',
secretAccessKey: 'secret-access-key-from-integrations',
},
{
accessKeyId: 'access-key-from-integrations-2',
secretAccessKey: 'secret-access-key-from-integrations-2',
},
],
},
});
await IsolatedAwsS3Publish.fromConfig(mockConfig, logger);
expect(getCredProviderMock).toHaveBeenCalledTimes(0);
expect(MockS3Client).toHaveBeenCalledTimes(1);
await expect(
MockS3Client.mock.calls[0][0]!.credentialDefaultProvider!(
undefined!,
)(),
).resolves.toEqual({
accessKeyId: 'access-key-from-integrations-2',
secretAccessKey: 'secret-access-key-from-integrations-2',
});
});
});
});
@@ -139,12 +139,19 @@ export class AwsS3Publish implements PublisherBase {
const credentialsConfig = config.getOptionalConfig(
'techdocs.publisher.awsS3.credentials',
);
const credsManager = DefaultAwsCredentialsManager.fromConfig(config);
const awsS3IntegrationConfig =
config.getOptionalConfigArray('integrations.awsS3');
const sdkCredentialProvider = await AwsS3Publish.buildCredentials(
credsManager,
accountId,
credentialsConfig,
region,
awsS3IntegrationConfig,
logger,
);
// AWS endpoint is an optional config. If missing, the default endpoint is built from
@@ -213,8 +220,10 @@ export class AwsS3Publish implements PublisherBase {
private static async buildCredentials(
credsManager: AwsCredentialsManager,
accountId?: string,
config?: Config,
credentialsConfig?: Config,
region?: string,
awsS3IntegrationConfig?: Config[],
logger: LoggerService,
): Promise<AwsCredentialIdentityProvider> {
// Pull credentials for the specified account ID from the 'aws' config section
if (accountId) {
@@ -222,21 +231,14 @@ export class AwsS3Publish implements PublisherBase {
.sdkCredentialProvider;
}
// Fall back to the default credential chain if neither account ID
// nor explicit credentials are provided
if (!config) {
return (await credsManager.getCredentialProvider()).sdkCredentialProvider;
}
const explicitCredentials = await AwsS3Publish.getExplicitCredentials({
credsManager,
credentialsConfig,
awsS3IntegrationConfig,
logger,
});
// Pull credentials from the techdocs config section (deprecated)
const accessKeyId = config.getOptionalString('accessKeyId');
const secretAccessKey = config.getOptionalString('secretAccessKey');
const explicitCredentials: AwsCredentialIdentityProvider =
accessKeyId && secretAccessKey
? AwsS3Publish.buildStaticCredentials(accessKeyId, secretAccessKey)
: (await credsManager.getCredentialProvider()).sdkCredentialProvider;
const roleArn = config.getOptionalString('roleArn');
const roleArn = credentialsConfig?.getOptionalString('roleArn');
if (roleArn) {
return fromTemporaryCredentials({
masterCredentials: explicitCredentials,
@@ -251,6 +253,72 @@ export class AwsS3Publish implements PublisherBase {
return explicitCredentials;
}
private static async getExplicitCredentials({
credentialsConfig,
awsS3IntegrationConfig,
credsManager,
logger,
}: {
credentialsConfig?: Config;
awsS3IntegrationConfig?: Config[];
credsManager: AwsCredentialsManager;
logger: LoggerService;
}): Promise<AwsCredentialIdentityProvider> {
const accessKeyId = credentialsConfig?.getOptionalString('accessKeyId');
const secretAccessKey =
credentialsConfig?.getOptionalString('secretAccessKey');
if (accessKeyId && secretAccessKey) {
return AwsS3Publish.buildStaticCredentials(accessKeyId, secretAccessKey);
}
if (awsS3IntegrationConfig && awsS3IntegrationConfig.length > 0) {
if (awsS3IntegrationConfig.length === 1) {
const singleAwsS3IntegrationConfig = awsS3IntegrationConfig[0];
const singleAwsS3IntegrationAccessKeyId =
singleAwsS3IntegrationConfig.getOptionalString('accessKeyId');
const singleAwsS3IntegrationSecretAccessKey =
singleAwsS3IntegrationConfig.getOptionalString('secretAccessKey');
if (
singleAwsS3IntegrationAccessKeyId &&
singleAwsS3IntegrationSecretAccessKey
) {
return AwsS3Publish.buildStaticCredentials(
singleAwsS3IntegrationAccessKeyId,
singleAwsS3IntegrationSecretAccessKey,
);
}
} else {
if (accessKeyId) {
const targetAwsS3IntegrationConfig = awsS3IntegrationConfig.find(
c => c.getOptionalString('accessKeyId') === accessKeyId,
);
if (!targetAwsS3IntegrationConfig) {
logger.warn(
`No AWS S3 integration config under integrations.awsS3 found for access key id ${accessKeyId}.`,
);
}
const targetAwsS3IntegrationAccessKeyId =
targetAwsS3IntegrationConfig?.getOptionalString('accessKeyId');
const targetAwsS3IntegrationSecretAccessKey =
targetAwsS3IntegrationConfig?.getOptionalString('secretAccessKey');
if (
targetAwsS3IntegrationAccessKeyId &&
targetAwsS3IntegrationSecretAccessKey
) {
return AwsS3Publish.buildStaticCredentials(
targetAwsS3IntegrationAccessKeyId,
targetAwsS3IntegrationSecretAccessKey,
);
}
}
}
}
return (await credsManager.getCredentialProvider()).sdkCredentialProvider;
}
/**
* Check if the defined bucket exists. Being able to connect means the configuration is good
* and the storage client will work.