diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfiguration.java index f196e31db72..a6a9e9e3831 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfiguration.java @@ -16,11 +16,15 @@ package org.springframework.ai.model.bedrock.autoconfigure; +import java.nio.file.Paths; + import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.providers.AwsRegionProvider; import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; @@ -36,6 +40,7 @@ * * @author Christian Tzolov * @author Wei Jiang + * @author Baojun Jiang */ @Configuration @EnableConfigurationProperties(BedrockAwsConnectionProperties.class) @@ -44,19 +49,48 @@ public class BedrockAwsConnectionConfiguration { @Bean @ConditionalOnMissingBean public AwsCredentialsProvider credentialsProvider(BedrockAwsConnectionProperties properties) { - if (StringUtils.hasText(properties.getAccessKey()) && StringUtils.hasText(properties.getSecretKey())) { - + // Security key if (StringUtils.hasText(properties.getSessionToken())) { return StaticCredentialsProvider.create(AwsSessionCredentials.create(properties.getAccessKey(), properties.getSecretKey(), properties.getSessionToken())); } - return StaticCredentialsProvider .create(AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())); } - - return DefaultCredentialsProvider.create(); + else if (properties.getProfile() != null && StringUtils.hasText(properties.getProfile().getName())) { + // Profile + ProfileProperties profile = properties.getProfile(); + String configurationPath = profile.getConfigurationPath(); + String credentialsPath = profile.getCredentialsPath(); + boolean hasCredentials = StringUtils.hasText(credentialsPath); + boolean hasConfig = StringUtils.hasText(configurationPath); + ProfileCredentialsProvider.Builder providerBuilder = ProfileCredentialsProvider.builder(); + if (hasCredentials || hasConfig) { + ProfileFile.Aggregator aggregator = ProfileFile.aggregator(); + if (hasCredentials) { + ProfileFile profileFile = ProfileFile.builder() + .content(Paths.get(credentialsPath)) + .type(ProfileFile.Type.CREDENTIALS) + .build(); + aggregator.addFile(profileFile); + } + if (hasConfig) { + ProfileFile configFile = ProfileFile.builder() + .content(Paths.get(configurationPath)) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + aggregator.addFile(configFile); + } + ProfileFile aggregatedProfileFile = aggregator.build(); + providerBuilder.profileFile(aggregatedProfileFile); + } + return providerBuilder.profileName(profile.getName()).build(); + } + else { + // Default: IAM Role, System Environment, etc. + return DefaultCredentialsProvider.builder().build(); + } } @Bean diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionProperties.java index 2b16b5e6a9a..3dd1e9f31a5 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionProperties.java @@ -19,11 +19,13 @@ import java.time.Duration; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; /** * Configuration properties for Bedrock AWS connection. * * @author Christian Tzolov + * @author Baojun Jiang * @since 0.8.0 */ @ConfigurationProperties(BedrockAwsConnectionProperties.CONFIG_PREFIX) @@ -48,10 +50,17 @@ public class BedrockAwsConnectionProperties { /** * AWS session token. (optional) When provided the AwsSessionCredentials are used. - * Otherwise the AwsBasicCredentials are used. + * Otherwise, the AwsBasicCredentials are used. */ private String sessionToken; + /** + * Aws profile. (optional) When the {@link #accessKey} and {@link #secretKey} are not + * declared. Otherwise, the AwsBasicCredentials are used. + */ + @NestedConfigurationProperty + private ProfileProperties profile; + /** * Maximum duration of the entire API call operation. */ @@ -149,4 +158,12 @@ public void setSessionToken(String sessionToken) { this.sessionToken = sessionToken; } + public ProfileProperties getProfile() { + return this.profile; + } + + public void setProfile(ProfileProperties profile) { + this.profile = profile; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/ProfileProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/ProfileProperties.java new file mode 100644 index 00000000000..7e8681f61b4 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/ProfileProperties.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.model.bedrock.autoconfigure; + +/** + * Configuration properties for Bedrock AWS connection using profile. + * + * @author Baojun Jiang + */ +public class ProfileProperties { + + /** + * Name of the profile to use. + */ + private String name; + + /** + * (optional) Path to the credentials file. default: ~/.aws/credentials + */ + private String credentialsPath; + + /** + * (optional) Path to the configuration file. default: ~/.aws/config + */ + private String configurationPath; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCredentialsPath() { + return this.credentialsPath; + } + + public void setCredentialsPath(String credentialsPath) { + this.credentialsPath = credentialsPath; + } + + public String getConfigurationPath() { + return this.configurationPath; + } + + public void setConfigurationPath(String configurationPath) { + this.configurationPath = configurationPath; + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfigurationIT.java index 7409fcef8ef..ff8b7d62578 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfigurationIT.java @@ -16,9 +16,15 @@ package org.springframework.ai.model.bedrock.autoconfigure; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; + import org.junit.jupiter.api.Test; import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.providers.AwsRegionProvider; @@ -63,7 +69,8 @@ public void autoConfigureAWSCredentialAndRegionProvider() { public void autoConfigureWithCustomAWSCredentialAndRegionProvider() { BedrockTestUtils.getContextRunner() .withConfiguration(AutoConfigurations.of(TestAutoConfiguration.class, - CustomAwsCredentialsProviderAndAwsRegionProviderAutoConfiguration.class)) + CustomAwsCredentialsProviderAutoConfiguration.class, + CustomAwsRegionProviderAutoConfiguration.class)) .run(context -> { var awsCredentialsProvider = context.getBean(AwsCredentialsProvider.class); var awsRegionProvider = context.getBean(AwsRegionProvider.class); @@ -80,6 +87,30 @@ public void autoConfigureWithCustomAWSCredentialAndRegionProvider() { }); } + @Test + public void autoConfigureWithCustomAWSProfileCredentialAndRegionProvider() { + BedrockTestUtils.getContextRunner() + .withConfiguration(AutoConfigurations.of(TestAutoConfiguration.class, + CustomAwsProfileCredentialsProviderAutoConfiguration.class, + CustomAwsRegionProviderAutoConfiguration.class)) + .run(context -> { + var awsCredentialsProvider = context.getBean(AwsCredentialsProvider.class); + var awsRegionProvider = context.getBean(AwsRegionProvider.class); + + assertThat(awsCredentialsProvider).isNotNull(); + assertThat(awsRegionProvider).isNotNull(); + + assertThat(awsCredentialsProvider).isInstanceOf(ProfileCredentialsProvider.class); + // aws sdk2.x does not provide method to get profileName, use reflection + // to get + Field field = ProfileCredentialsProvider.class.getDeclaredField("profileName"); + field.setAccessible(true); + assertThat(field.get(awsCredentialsProvider)).isEqualTo("CUSTOM_PROFILE_NAME"); + + assertThat(awsRegionProvider.getRegion()).isEqualTo(Region.AWS_GLOBAL); + }); + } + @EnableConfigurationProperties(BedrockAwsConnectionProperties.class) @Import(BedrockAwsConnectionConfiguration.class) static class TestAutoConfiguration { @@ -87,7 +118,42 @@ static class TestAutoConfiguration { } @AutoConfiguration - static class CustomAwsCredentialsProviderAndAwsRegionProviderAutoConfiguration { + static class CustomAwsProfileCredentialsProviderAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public AwsCredentialsProvider credentialsProvider() { + String credentialsPath = "CUSTOM_CREDENTIALS_PATH"; + String configurationPath = "CUSTOM_CONFIGURATION_PATH"; + boolean hasCredentials = Files.exists(Paths.get(credentialsPath)); + boolean hasConfig = Files.exists(Paths.get(configurationPath)); + ProfileCredentialsProvider.Builder providerBuilder = ProfileCredentialsProvider.builder(); + if (hasCredentials || hasConfig) { + ProfileFile.Aggregator aggregator = ProfileFile.aggregator(); + if (hasCredentials) { + ProfileFile profileFile = ProfileFile.builder() + .content(Paths.get(credentialsPath)) + .type(ProfileFile.Type.CREDENTIALS) + .build(); + aggregator.addFile(profileFile); + } + if (hasConfig) { + ProfileFile configFile = ProfileFile.builder() + .content(Paths.get(configurationPath)) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + aggregator.addFile(configFile); + } + ProfileFile aggregatedProfileFile = aggregator.build(); + providerBuilder.profileFile(aggregatedProfileFile); + } + return providerBuilder.profileName("CUSTOM_PROFILE_NAME").build(); + } + + } + + @AutoConfiguration + static class CustomAwsCredentialsProviderAutoConfiguration { @Bean @ConditionalOnMissingBean @@ -114,6 +180,11 @@ public String secretAccessKey() { }; } + } + + @AutoConfiguration + static class CustomAwsRegionProviderAutoConfiguration { + @Bean @ConditionalOnMissingBean public AwsRegionProvider regionProvider() { diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock.adoc index 1723128457d..71badfdb899 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock.adoc @@ -64,6 +64,10 @@ spring.ai.bedrock.aws.region=us-east-1 spring.ai.bedrock.aws.access-key=YOUR_ACCESS_KEY spring.ai.bedrock.aws.secret-key=YOUR_SECRET_KEY +spring.ai.bedrock.aws.profile.name=YOUR_PROFILE_NAME +spring.ai.bedrock.aws.profile.credentials-path=YOUR_CREDENTIALS_PATH +spring.ai.bedrock.aws.profile.configuration-path=YOUR_CONFIGURATION_PATH + spring.ai.bedrock.aws.timeout=10m ---- @@ -72,12 +76,13 @@ The `region` property is compulsory. AWS credentials are resolved in the following order: 1. Spring-AI Bedrock `spring.ai.bedrock.aws.access-key` and `spring.ai.bedrock.aws.secret-key` properties. -2. Java System Properties - `aws.accessKeyId` and `aws.secretAccessKey`. -3. Environment Variables - `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. -4. Web Identity Token credentials from system properties or environment variables. -5. Credential profiles file at the default location (`~/.aws/credentials`) shared by all AWS SDKs and the AWS CLI. -6. Credentials delivered through the Amazon EC2 container service if the `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` environment variable is set and the security manager has permission to access the variable. -7. Instance profile credentials delivered through the Amazon EC2 metadata service or set the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. +2. Spring-AI Bedrock `spring.ai.bedrock.aws.profile.name`, If `spring.ai.bedrock.aws.profile.credentials-path` and `spring.ai.bedrock.aws.profile.configuration-path` are not specified, Spring AI use the standard AWS shared files: `~/.aws/credentials` for credentials and `~/.aws/config` for configuration. +3. Java System Properties - `aws.accessKeyId` and `aws.secretAccessKey`. +4. Environment Variables - `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. +5. Web Identity Token credentials from system properties or environment variables. +6. Credential profiles file at the default location (`~/.aws/credentials`) shared by all AWS SDKs and the AWS CLI. +7. Credentials delivered through the Amazon EC2 container service if the `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` environment variable is set and the security manager has permission to access the variable. +8. Instance profile credentials delivered through the Amazon EC2 metadata service or set the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. AWS region is resolved in the following order: diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/bedrock-converse.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/bedrock-converse.adoc index 1a151dabc76..2ae30f24215 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/bedrock-converse.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/bedrock-converse.adoc @@ -81,6 +81,9 @@ The prefix `spring.ai.bedrock.aws` is the property prefix to configure the conne | spring.ai.bedrock.aws.access-key | AWS access key | - | spring.ai.bedrock.aws.secret-key | AWS secret key | - | spring.ai.bedrock.aws.session-token | AWS session token for temporary credentials | - +| spring.ai.bedrock.aws.profile.name | AWS profile name. | - +| spring.ai.bedrock.aws.profile.credentials-path | AWS credentials file path. | - +| spring.ai.bedrock.aws.profile.configuration-path | AWS config file path. | - |==== [NOTE] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-cohere-embedding.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-cohere-embedding.adoc index 6b67b31c7fd..e75905bae4c 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-cohere-embedding.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-cohere-embedding.adoc @@ -90,6 +90,9 @@ The prefix `spring.ai.bedrock.aws` is the property prefix to configure the conne | spring.ai.bedrock.aws.region | AWS region to use. | us-east-1 | spring.ai.bedrock.aws.access-key | AWS access key. | - | spring.ai.bedrock.aws.secret-key | AWS secret key. | - +| spring.ai.bedrock.aws.profile.name | AWS profile name. | - +| spring.ai.bedrock.aws.profile.credentials-path | AWS credentials file path. | - +| spring.ai.bedrock.aws.profile.configuration-path | AWS config file path. | - |==== [NOTE] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-titan-embedding.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-titan-embedding.adoc index 2cce947c316..b49df125ee1 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-titan-embedding.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/bedrock-titan-embedding.adoc @@ -97,6 +97,9 @@ The prefix `spring.ai.bedrock.aws` is the property prefix to configure the conne | spring.ai.bedrock.aws.region | AWS region to use. | us-east-1 | spring.ai.bedrock.aws.access-key | AWS access key. | - | spring.ai.bedrock.aws.secret-key | AWS secret key. | - +| spring.ai.bedrock.aws.profile.name | AWS profile name. | - +| spring.ai.bedrock.aws.profile.credentials-path | AWS credentials file path. | - +| spring.ai.bedrock.aws.profile.configuration-path | AWS config file path. | - |==== [NOTE]