Skip to content

Commit 90bf752

Browse files
Merge branch 'main' into oneOf
2 parents 20432d1 + a2cc221 commit 90bf752

File tree

16 files changed

+495
-217
lines changed

16 files changed

+495
-217
lines changed

.github/workflows/fosstars-report.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ jobs:
2222
distribution: "temurin"
2323
java-version: ${{ env.JAVA_VERSION }}
2424
cache: 'maven'
25+
- name: Restore CVE Database
26+
uses: actions/cache/restore@v4
27+
with:
28+
path: ${{ env.CVE_CACHE_DIR }}
29+
key: ${{ env.CVE_CACHE_KEY }}
30+
fail-on-cache-miss: true
2531

2632
- name: "Build SDK"
2733
run: |
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Update Vulnerability Database
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: '42 03 * * MON-FRI' # 03:42 on weekdays, a somewhat random time to avoid producing load spikes on the GH actions infrastructure
7+
8+
env:
9+
CVE_CACHE_REF: refs/heads/main
10+
CVE_CACHE_KEY: cve-db
11+
CVE_CACHE_DIR: ~/.m2/repository/org/owasp/dependency-check-data
12+
13+
jobs:
14+
update-vulnerability-database:
15+
runs-on: ubuntu-latest
16+
permissions:
17+
contents: write
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
ref: ${{ env.CVE_CACHE_REF }}
22+
- name: Restore Existing Cache
23+
uses: actions/cache/restore@v4
24+
with:
25+
path: ${{ env.CVE_CACHE_DIR }}
26+
key: ${{ env.CVE_CACHE_KEY }}
27+
28+
- name: Run Maven Plugin
29+
run: |
30+
mvn org.owasp:dependency-check-maven:10.0.4:update-only -DnvdMaxRetryCount=10 -DnvdApiDelay=15000 -DconnectionTimeout=60000
31+
env:
32+
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
33+
34+
- name: Delete Cache
35+
run: |
36+
CACHE_IDS=$(gh cache list --key "${{ env.CVE_CACHE_KEY }}" --ref "${{ env.CVE_CACHE_REF }}" --json id | jq -r '.[] | .id')
37+
for CACHE_ID in $CACHE_IDS; do
38+
echo "Deleting cache with ID: $CACHE_ID"
39+
gh cache delete "${CACHE_ID}"
40+
done
41+
env:
42+
GH_TOKEN: ${{ secrets.CLOUD_SDK_AT_SAP_ALL_ACCESS_PAT }}
43+
44+
- name: Cache CVE Database
45+
uses: actions/cache/save@v4
46+
with:
47+
path: ${{ env.CVE_CACHE_DIR }}
48+
key: ${{ env.CVE_CACHE_KEY }}
49+
50+
# - name: "Slack Notification"
51+
# if: failure()
52+
# uses: slackapi/[email protected]
53+
# with:
54+
# payload: |
55+
# {
56+
# "text": "⚠️ OWASP Update Failed! 😬 Please inspect & fix by clicking <https://github.com/SAP/ai-sdk-java/actions/runs/${{ github.run_id }}|here>"
57+
# }
58+
# env:
59+
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
60+
# SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
61+
62+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.sap.ai.sdk.core;
2+
3+
import lombok.experimental.StandardException;
4+
5+
/** Exception thrown when the JSON AI Core service key is invalid. */
6+
@StandardException
7+
class AiCoreCredentialsInvalidException extends RuntimeException {}

core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.sap.ai.sdk.core;
22

3-
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
3+
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
44
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
55
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException;
66
import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient;
@@ -30,7 +30,8 @@ public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) {
3030

3131
@Nonnull
3232
@Override
33-
public Destination destination() throws DestinationAccessException, DestinationNotFoundException {
33+
public HttpDestination destination()
34+
throws DestinationAccessException, DestinationNotFoundException {
3435
aiCoreService.deploymentId = deploymentId.get();
3536
return aiCoreService.destination();
3637
}

core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package com.sap.ai.sdk.core;
22

3-
import static com.sap.ai.sdk.core.DestinationResolver.AI_CLIENT_TYPE_KEY;
4-
import static com.sap.ai.sdk.core.DestinationResolver.AI_CLIENT_TYPE_VALUE;
5-
63
import com.fasterxml.jackson.annotation.JsonAutoDetect;
74
import com.fasterxml.jackson.annotation.JsonInclude;
85
import com.fasterxml.jackson.annotation.PropertyAccessor;
@@ -12,15 +9,14 @@
129
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
1310
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
1411
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperty;
12+
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
1513
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
1614
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException;
1715
import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient;
18-
import io.github.cdimascio.dotenv.Dotenv;
1916
import java.util.NoSuchElementException;
2017
import java.util.function.BiFunction;
2118
import java.util.function.Function;
2219
import javax.annotation.Nonnull;
23-
import lombok.RequiredArgsConstructor;
2420
import lombok.extern.slf4j.Slf4j;
2521
import lombok.val;
2622
import org.springframework.http.client.BufferingClientHttpRequestFactory;
@@ -31,19 +27,21 @@
3127

3228
/** Connectivity convenience methods for AI Core. */
3329
@Slf4j
34-
@RequiredArgsConstructor
3530
public class AiCoreService implements AiCoreDestination {
36-
37-
Function<AiCoreService, Destination> baseDestinationHandler;
38-
final BiFunction<AiCoreService, Destination, ApiClient> clientHandler;
39-
final BiFunction<AiCoreService, Destination, DefaultHttpDestination.Builder> builderHandler;
31+
static final String AI_CLIENT_TYPE_KEY = "URL.headers.AI-Client-Type";
32+
static final String AI_CLIENT_TYPE_VALUE = "AI SDK Java";
33+
static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group";
4034

4135
private static final DeploymentCache DEPLOYMENT_CACHE = new DeploymentCache();
4236

43-
private static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group";
37+
@Nonnull private final DestinationResolver destinationResolver;
4438

45-
/** loads the .env file from the root of the project */
46-
private static final Dotenv DOTENV = Dotenv.configure().ignoreIfMissing().load();
39+
@Nonnull private Function<AiCoreService, HttpDestination> baseDestinationHandler;
40+
@Nonnull private final BiFunction<AiCoreService, HttpDestination, ApiClient> clientHandler;
41+
42+
@Nonnull
43+
private final BiFunction<AiCoreService, HttpDestination, DefaultHttpDestination.Builder>
44+
builderHandler;
4745

4846
/** The resource group is defined by AiCoreDeployment.withResourceGroup(). */
4947
@Nonnull String resourceGroup;
@@ -53,8 +51,16 @@ public class AiCoreService implements AiCoreDestination {
5351

5452
/** The default constructor. */
5553
public AiCoreService() {
56-
this(AiCoreService::getApiClient, AiCoreService::getDestinationBuilder, "default", "");
54+
this(new DestinationResolver());
55+
}
56+
57+
AiCoreService(@Nonnull final DestinationResolver destinationResolver) {
58+
this.destinationResolver = destinationResolver;
5759
baseDestinationHandler = AiCoreService::getBaseDestination;
60+
clientHandler = AiCoreService::buildApiClient;
61+
builderHandler = AiCoreService::getDestinationBuilder;
62+
resourceGroup = "default";
63+
deploymentId = "";
5864
}
5965

6066
@Nonnull
@@ -66,7 +72,7 @@ public ApiClient client() {
6672

6773
@Nonnull
6874
@Override
69-
public Destination destination() {
75+
public HttpDestination destination() {
7076
val dest = baseDestinationHandler.apply(this);
7177
val builder = builderHandler.apply(this, dest);
7278
if (!deploymentId.isEmpty()) {
@@ -107,7 +113,7 @@ protected void destinationSetHeaders(@Nonnull final DefaultHttpDestination.Build
107113
* @return The AI Core Service based on the provided destination.
108114
*/
109115
@Nonnull
110-
public AiCoreService withDestination(@Nonnull final Destination destination) {
116+
public AiCoreService withDestination(@Nonnull final HttpDestination destination) {
111117
baseDestinationHandler = service -> destination;
112118
return this;
113119
}
@@ -161,10 +167,9 @@ public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId
161167
* @throws DestinationNotFoundException If the destination cannot be found.
162168
*/
163169
@Nonnull
164-
protected Destination getBaseDestination()
170+
protected HttpDestination getBaseDestination()
165171
throws DestinationAccessException, DestinationNotFoundException {
166-
val serviceKey = DOTENV.get("AICORE_SERVICE_KEY");
167-
return DestinationResolver.getDestination(serviceKey);
172+
return destinationResolver.getDestination();
168173
}
169174

170175
/**
@@ -186,14 +191,13 @@ protected DefaultHttpDestination.Builder getDestinationBuilder(
186191
}
187192

188193
/**
189-
* Get a destination using the default service binding loading logic.
194+
* Build an {@link ApiClient} that can be used for executing plain REST HTTP calls.
190195
*
191-
* @return The destination.
192-
* @throws DestinationAccessException If the destination cannot be accessed.
193-
* @throws DestinationNotFoundException If the destination cannot be found.
196+
* @param destination The destination to use as basis for the client.
197+
* @return The new API client.
194198
*/
195199
@Nonnull
196-
protected ApiClient getApiClient(@Nonnull final Destination destination) {
200+
protected ApiClient buildApiClient(@Nonnull final Destination destination) {
197201
val objectMapper =
198202
new Jackson2ObjectMapperBuilder()
199203
.modules(new JavaTimeModule())
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.sap.ai.sdk.core;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.core.type.TypeReference;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingBuilder;
7+
import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
8+
import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor;
9+
import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier;
10+
import com.sap.cloud.environment.servicebinding.api.exception.ServiceBindingAccessException;
11+
import io.github.cdimascio.dotenv.Dotenv;
12+
import io.github.cdimascio.dotenv.DotenvBuilder;
13+
import io.vavr.Lazy;
14+
import java.util.HashMap;
15+
import java.util.List;
16+
import javax.annotation.Nonnull;
17+
import lombok.AllArgsConstructor;
18+
import lombok.extern.slf4j.Slf4j;
19+
import lombok.val;
20+
21+
/**
22+
* Loads an AI Core service key from the "AICORE_SERVICE_KEY" environment variable with support for
23+
* .env files. Will ignore missing or malformed dotenv files.
24+
*/
25+
@Slf4j
26+
@AllArgsConstructor
27+
class AiCoreServiceKeyAccessor implements ServiceBindingAccessor {
28+
static final String ENV_VAR_KEY = "AICORE_SERVICE_KEY";
29+
30+
private final Lazy<Dotenv> dotenv;
31+
32+
AiCoreServiceKeyAccessor() {
33+
this(Dotenv.configure().ignoreIfMissing().ignoreIfMalformed());
34+
}
35+
36+
AiCoreServiceKeyAccessor(@Nonnull final DotenvBuilder dotenvBuilder) {
37+
dotenv = Lazy.of(dotenvBuilder::load);
38+
}
39+
40+
@Nonnull
41+
@Override
42+
public List<ServiceBinding> getServiceBindings() throws ServiceBindingAccessException {
43+
final String serviceKey;
44+
try {
45+
serviceKey = dotenv.get().get(ENV_VAR_KEY);
46+
} catch (Exception e) {
47+
throw new ServiceBindingAccessException("Failed to load service key from environment", e);
48+
}
49+
if (serviceKey == null) {
50+
log.debug("No service key found in environment variable {}", ENV_VAR_KEY);
51+
return List.of();
52+
}
53+
log.info(
54+
"""
55+
Found a service key in environment variable {}.
56+
Using a service key is recommended for local testing only.
57+
Bind the AI Core service to the application for productive usage.
58+
""",
59+
ENV_VAR_KEY);
60+
61+
val binding = createServiceBinding(serviceKey);
62+
return List.of(binding);
63+
}
64+
65+
static ServiceBinding createServiceBinding(@Nonnull final String serviceKey)
66+
throws ServiceBindingAccessException {
67+
final HashMap<String, Object> credentials;
68+
try {
69+
credentials = new ObjectMapper().readValue(serviceKey, new TypeReference<>() {});
70+
} catch (JsonProcessingException e) {
71+
throw new ServiceBindingAccessException(
72+
new AiCoreCredentialsInvalidException(
73+
"Error in parsing service key from the " + ENV_VAR_KEY + " environment variable.",
74+
e));
75+
}
76+
if (credentials.get("clientid") == null) {
77+
// explicitly check for the client ID to improve the error message
78+
// otherwise we get a rather generic DestinationNotFoundException error that none of the
79+
// loaders matched the binding
80+
throw new ServiceBindingAccessException(
81+
new AiCoreCredentialsInvalidException("Missing clientid in service key"));
82+
}
83+
84+
return new DefaultServiceBindingBuilder()
85+
.withServiceIdentifier(ServiceIdentifier.AI_CORE)
86+
.withCredentials(credentials)
87+
.build();
88+
}
89+
}

0 commit comments

Comments
 (0)