Skip to content
Open
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
1 change: 1 addition & 0 deletions service/tools/maven-plugin/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The plugin uses a goal prefix of `timefold` (see the plugin configuration in the
- Fetches platform identity/config by calling GET /api/platform/v1/aboutme?includeConfig=true
- Requires a platform personal access token available via environment variable `TIMEFOLD_PAT` (see "Authentication" below)
- If a single accountId is returned by the platform and `timefold.accountId` wasn't provided, the plugin will use it
- Fails the build when the account id can neither be taken from `timefold.accountId` nor derived from the platform response, i.e. when the personal access token is associated with no account or with several of them. In the latter case `timefold.accountId` has to be set explicitly
- Writes a properties file at `target/generated-resources/timefold-build.properties` with entries such as:
** `quarkus.container-image.registry` — value taken from platform config.containerRegistry
** `quarkus.container-image.group` — the account id used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import java.nio.file.Paths;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

import ai.timefold.solver.tools.maven.client.PlatformIdentityInfo;

Expand Down Expand Up @@ -100,12 +102,9 @@
if (info == null || !info.hasPushAccessRights()) {
throw new RuntimeException("No access to deploy model on Timefold Platform");
}
String accountId = getPropertyOrParameter(PROP_ACCOUNT_ID, this.accountId);
if (accountId == null && info.accountIds().size() == 1) {
accountId = info.accountIds().iterator().next();
}
String accountId = resolveAccountId(info);

Check warning on line 105 in service/tools/maven-plugin/src/main/java/ai/timefold/solver/tools/maven/ConfigureMojo.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename "accountId" which hides the field declared at line 64.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ_5CUFnWa2l343xhQZB&open=AZ_5CUFnWa2l343xhQZB&pullRequest=2589

if (accountId != null && !info.hasAccessToAccountId(accountId)) {
if (!info.hasAccessToAccountId(accountId)) {
throw new RuntimeException(
"No access to configured account id " + accountId + " or account not configured");
}
Expand Down Expand Up @@ -159,6 +158,45 @@
}
}

/**
* Resolves the account id the model is deployed under, which becomes the group of the container image. It is either
* configured explicitly, or, when the personal access token is associated with exactly one account, that account.
*
* @throws MojoFailureException when the account id is neither configured nor unambiguously derivable from the
* personal access token; without it the container image cannot be named, so the build must not continue.
*/
protected String resolveAccountId(PlatformIdentityInfo info) throws MojoFailureException {
String configuredAccountId = getPropertyOrParameter(PROP_ACCOUNT_ID, this.accountId);
if (configuredAccountId != null && !configuredAccountId.isBlank()) {
return configuredAccountId.trim();
}

Set<String> accountIds = info.accountIds() == null ? Set.of() : info.accountIds();
if (accountIds.size() == 1) {
return accountIds.iterator().next();
Comment thread
cristianonicolai marked this conversation as resolved.
}

if (accountIds.isEmpty()) {
throw new MojoFailureException("""
Unable to resolve the Timefold Platform account id: the personal access token is not associated with \
any account, so the container image of this model cannot be built.
Use a personal access token of an account that is allowed to deploy models.
See https://docs.timefold.ai/timefold-solver/latest/deploying-to-platform/guide""");
}
throw new MojoFailureException("""
Unable to resolve the Timefold Platform account id: the personal access token is associated with %d \
accounts (%s), so the account to deploy this model to has to be configured explicitly.
Either pass it on the command line:
mvn clean package -D%s=<account id> timefold:deploy
or declare it in the plugin configuration:
<configuration>
<accountId>...</accountId>
</configuration>
See https://docs.timefold.ai/timefold-solver/latest/deploying-to-platform/guide"""
.formatted(accountIds.size(), accountIds.stream().sorted().collect(Collectors.joining(", ")),
PROP_ACCOUNT_ID));
}

/**
* Timefold Platform only accepts models that inherit from {@code timefold-solver-service-parent} and that are built
* with the Enterprise Edition. Such a model builds and deploys successfully, but fails later, when it actually runs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,59 @@
}
}
""")));

// the token is allowed to deploy models, but is not associated with any account
wm1.stubFor(get(urlPathEqualTo("/api/platform/v1/aboutme"))
.withHeader("Authorization", equalTo("Bearer noaccounts"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"user" : "test@email.com",
"scopes" : ["registered-model:create"],
"tenants" : [],
"accountIds" : [],
"config" : {
"containerRegistry" : "test.registry.com"
}
}
""")));

// the platform does not report the accountIds field at all
wm1.stubFor(get(urlPathEqualTo("/api/platform/v1/aboutme"))
.withHeader("Authorization", equalTo("Bearer noaccountids"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"user" : "test@email.com",
"scopes" : ["registered-model:create"],
"tenants" : [],
"config" : {
"containerRegistry" : "test.registry.com"
}
}
""")));

// the token is associated with several accounts, so the account id cannot be derived from it
wm1.stubFor(get(urlPathEqualTo("/api/platform/v1/aboutme"))
.withHeader("Authorization", equalTo("Bearer multipleaccounts"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{
"user" : "test@email.com",
"scopes" : ["registered-model:create"],
"tenants" : [],
"accountIds" : ["test", "company"],
"config" : {
"containerRegistry" : "test.registry.com"
}
}
""")));
}

@Test
Expand Down Expand Up @@ -266,6 +319,79 @@
wm1.verify(1, getRequestedFor(urlPathEqualTo("/api/platform/v1/aboutme")));
}

@Test
@InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml")
public void testConfigureFailsWhenTokenHasNoAccount(ConfigureMojo mojo) {

session.getRequest().setGoals(List.of("timefold:deploy"));
setEnterpriseModel(mojo);

mojo.setAccessTokenProvider(new TestAccessTokenProvider("noaccounts"));
mojo.setLog(log);
mojo.platformUrl = wm1.getRuntimeInfo().getHttpBaseUrl();

assertThatThrownBy(() -> mojo.execute()).isInstanceOf(MojoFailureException.class)

Check warning on line 333 in service/tools/maven-plugin/src/test/java/ai/timefold/solver/tools/maven/ConfigureMojoTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this lambda with method reference 'mojo::execute'.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ_5CULhWa2l343xhQZC&open=AZ_5CULhWa2l343xhQZC&pullRequest=2589
.hasMessageContaining("Unable to resolve the Timefold Platform account id")
.hasMessageContaining("not associated with any account");

wm1.verify(1, getRequestedFor(urlPathEqualTo("/api/platform/v1/aboutme")));
}

@Test
@InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml")
public void testConfigureFailsWhenPlatformReportsNoAccountIds(ConfigureMojo mojo) {

session.getRequest().setGoals(List.of("timefold:deploy"));
setEnterpriseModel(mojo);

mojo.setAccessTokenProvider(new TestAccessTokenProvider("noaccountids"));
mojo.setLog(log);
mojo.platformUrl = wm1.getRuntimeInfo().getHttpBaseUrl();

assertThatThrownBy(() -> mojo.execute()).isInstanceOf(MojoFailureException.class)

Check warning on line 351 in service/tools/maven-plugin/src/test/java/ai/timefold/solver/tools/maven/ConfigureMojoTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this lambda with method reference 'mojo::execute'.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ_5CULhWa2l343xhQZD&open=AZ_5CULhWa2l343xhQZD&pullRequest=2589
.hasMessageContaining("not associated with any account");
}

@Test
@InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml")
public void testConfigureFailsWhenAccountIdIsAmbiguous(ConfigureMojo mojo) {

session.getRequest().setGoals(List.of("timefold:deploy"));
setEnterpriseModel(mojo);

mojo.setAccessTokenProvider(new TestAccessTokenProvider("multipleaccounts"));
mojo.setLog(log);
mojo.platformUrl = wm1.getRuntimeInfo().getHttpBaseUrl();

assertThatThrownBy(() -> mojo.execute()).isInstanceOf(MojoFailureException.class)

Check warning on line 366 in service/tools/maven-plugin/src/test/java/ai/timefold/solver/tools/maven/ConfigureMojoTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this lambda with method reference 'mojo::execute'.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ_5CULhWa2l343xhQZE&open=AZ_5CULhWa2l343xhQZE&pullRequest=2589
.hasMessageContaining("associated with 2 accounts (company, test)")
.hasMessageContaining("-Dtimefold.accountId=<account id>");
}

@Test
@MojoParameter(name = "accountId", value = "company")
@InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml")
public void testConfigureUsesConfiguredAccountIdWhenSeveralAreAvailable(ConfigureMojo mojo) throws Exception {

session.getRequest().setGoals(List.of("timefold:deploy"));
setEnterpriseModel(mojo);

mojo.setAccessTokenProvider(new TestAccessTokenProvider("multipleaccounts"));
mojo.setLog(log);
mojo.platformUrl = wm1.getRuntimeInfo().getHttpBaseUrl();
mojo.execute();

log.assertContains("Configured Timefold Platform integration", Level.INFO);

Path buildProperties = Paths.get("target", "generated-resources", "timefold-build.properties");

Properties props = new Properties();
try (InputStream in = Files.newInputStream(buildProperties)) {
props.load(in);
}
assertThat(props).containsEntry("quarkus.container-image.group", "company");
}

@Test
@InjectMojo(goal = "configure", pom = "src/test/resources/project-to-test/pom.xml")
public void testConfigureWrongScopes(ConfigureMojo mojo) throws Exception {
Expand Down
Loading