Skip to content

Commit

Permalink
Feature / Client config API (#491)
Browse files Browse the repository at this point in the history
* API and config structure for client config API

* Unit tests for client config API

* Implement client config API

* Client config property in unit test config

* Update gradle properties
  • Loading branch information
martin-traverse authored Jan 13, 2025
1 parent 5332d0b commit a49728c
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 19 deletions.
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# Allow for OWASP dependency check gradle task, which is going OOM
org.gradle.jvmargs=-Xmx4096m
# Flags to improve build performance
org.gradle.configureondemand=true
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ message PlatformConfig {
map<string, ServiceConfig> services = 4;

DeploymentConfig deployment = 14;

map<string, ClientConfig> clientConfig = 15;
}


Expand Down Expand Up @@ -145,3 +147,8 @@ enum DeploymentLayout {
HOSTED = 2;
CUSTOM = 3;
}

message ClientConfig {

map<string, string> properties = 1;
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,23 @@ service TracMetadataApi {
};
}

/**
* Get configuration for a client application, which is managed as pat of the TRAC deployment.
*
* Configuration properties for individual client applications can be set and managed as
* part of the TRAC platform configuration. Those properties are made available to
* authenticated client applications via this API call. There is currently no API to set
* these properties, they must be set by an administrator as part of the platform deployment.
*
* This API is typically accessed using the credentials of the currently logged-in user,
* it should not be used for sensitive configuration such as private keys or secrets.
*/
rpc clientConfig(ClientConfigRequest) returns (ClientConfigResponse) {
option (google.api.http) = {
get: "/trac/client-config/{application}"
};
}

/**
* Get a list of infrastructure resources of a given type.
*
Expand Down Expand Up @@ -517,6 +534,21 @@ message ListTenantsResponse {
repeated metadata.TenantInfo tenants = 1;
}

/**
* Request object for the clientConfig() API call.
*/
message ClientConfigRequest {

string application = 1;
}

/**
* Response object for the clientConfig() API call.
*/
message ClientConfigResponse {

map<string, string> properties = 1;
}

/**
* Request object for the listResources() API call.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,12 @@ public class MetadataConstants {
// ^...$ would allow matches like "my_var\n_gotcha"
public static final Pattern VALID_IDENTIFIER = Pattern.compile("\\A[a-zA-Z_]\\w*\\Z");

// Resource keys are similar to identifiers but can also include hyphens
public static final Pattern VALID_RESOURCE_KEY = Pattern.compile("\\A[a-zA-Z_\\-][a-zA-Z0-9_\\-]*\\Z");

// Identifiers starting trac_ are reserved for use by the TRAC platform
// Identifiers starting _ are also reserved by convention, for private / protected / system variables
public static final Pattern TRAC_RESERVED_IDENTIFIER = Pattern.compile("\\A(trac_|_).*", Pattern.CASE_INSENSITIVE);
public static final Pattern TRAC_RESERVED_IDENTIFIER = Pattern.compile("\\A(trac[_\\-.]|[_\\-.]).*", Pattern.CASE_INSENSITIVE);

public static final String TRAC_CREATE_TIME = "trac_create_time";
public static final String TRAC_CREATE_USER_ID = "trac_create_user_id";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,10 @@ services:

deployment:
layout: SANDBOX


clientConfig:

client-app:
properties:
unit.test.property: value1
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ public class MetadataApiValidator {
private static final Descriptors.FieldDescriptor RIR_RESOURCE_TYPE;
private static final Descriptors.FieldDescriptor RIR_RESOURCE_KEY;

private static final Descriptors.Descriptor CLIENT_CONFIG_REQUEST;
private static final Descriptors.FieldDescriptor CCR_APPLICATION;

static {

METADATA_WRITE_REQUEST = MetadataWriteRequest.getDescriptor();
Expand Down Expand Up @@ -127,6 +130,9 @@ public class MetadataApiValidator {
RIR_TENANT = field(RESOURCE_INFO_REQUEST, ResourceInfoRequest.TENANT_FIELD_NUMBER);
RIR_RESOURCE_TYPE = field(RESOURCE_INFO_REQUEST, ResourceInfoRequest.RESOURCETYPE_FIELD_NUMBER);
RIR_RESOURCE_KEY = field(RESOURCE_INFO_REQUEST, ResourceInfoRequest.RESOURCEKEY_FIELD_NUMBER);

CLIENT_CONFIG_REQUEST = ClientConfigRequest.getDescriptor();
CCR_APPLICATION = field(CLIENT_CONFIG_REQUEST, ClientConfigRequest.APPLICATION_FIELD_NUMBER);
}

@Validator(method = "createObject")
Expand Down Expand Up @@ -548,4 +554,13 @@ public static ValidationContext resourceInfo(ResourceInfoRequest msg, Validation

return ctx;
}

@Validator(method = "clientConfig")
public static ValidationContext clientConfig(ClientConfigRequest msg, ValidationContext ctx) {

return ctx.push(CCR_APPLICATION)
.apply(CommonValidators::required)
.apply(CommonValidators::resourceKey)
.pop();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,13 @@ public static ValidationContext identifier(String value, ValidationContext ctx)
"is not a valid identifier", value, ctx);
}

public static ValidationContext resourceKey(String value, ValidationContext ctx) {

return regexMatch(
MetadataConstants.VALID_RESOURCE_KEY, true,
"is not a valid resource key", value, ctx);
}

public static ValidationContext notTracReserved(String value, ValidationContext ctx) {

return regexMatch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ void listTenants(ListTenantsRequest request, StreamObserver<ListTenantsResponse>
}
}

void clientConfig(ClientConfigRequest request, StreamObserver<ClientConfigResponse> response) {

try {
var result = readService.clientConfig(request);
response.onNext(result);
response.onCompleted();
}
catch (Exception error) {
response.onError(error);
}
}

void listResources(ListResourcesRequest request, StreamObserver<ListResourcesResponse> response) {

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ public void listTenants(ListTenantsRequest request, StreamObserver<ListTenantsRe
apiImpl.listTenants(request, response);
}

@Override
public void clientConfig(ClientConfigRequest request, StreamObserver<ClientConfigResponse> responseObserver) {

apiImpl.clientConfig(request, responseObserver);
}

@Override
public void listResources(ListResourcesRequest request, StreamObserver<ListResourcesResponse> response) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@

package org.finos.tracdap.svc.meta.services;

import org.finos.tracdap.api.ListResourcesResponse;
import org.finos.tracdap.api.ListTenantsResponse;
import org.finos.tracdap.api.PlatformInfoResponse;
import org.finos.tracdap.api.ResourceInfoResponse;
import org.finos.tracdap.api.*;
import org.finos.tracdap.common.exception.EResourceNotFound;
import org.finos.tracdap.common.exception.ETenantNotFound;
import org.finos.tracdap.common.util.VersionInfo;
Expand All @@ -46,11 +43,11 @@ public class MetadataReadService {
private final Logger log = LoggerFactory.getLogger(getClass());

private final IMetadataDal dal;
private final PlatformConfig config;
private final PlatformConfig platformConfig;

public MetadataReadService(IMetadataDal dal, PlatformConfig platformConfig) {
this.dal = dal;
this.config = platformConfig;
this.platformConfig = platformConfig;
}

// Literally all the read logic is in the DAL at present!
Expand All @@ -60,9 +57,9 @@ public PlatformInfoResponse platformInfo() {

var tracVersion = VersionInfo.getComponentVersion(TracMetadataService.class);

var configInfo = config.getPlatformInfo();
var environment = configInfo.getEnvironment();
var production = configInfo.getProduction(); // defaults to false
var platformInfo = platformConfig.getPlatformInfo();
var environment = platformInfo.getEnvironment();
var production = platformInfo.getProduction(); // defaults to false

// TODO: Validate environment is set during startup
if (environment.isBlank())
Expand All @@ -72,7 +69,7 @@ public PlatformInfoResponse platformInfo() {
.setTracVersion(tracVersion)
.setEnvironment(environment)
.setProduction(production)
.putAllDeploymentInfo(configInfo.getDeploymentInfoMap())
.putAllDeploymentInfo(platformInfo.getDeploymentInfoMap())
.build();
}

Expand All @@ -85,6 +82,21 @@ public ListTenantsResponse listTenants() {
.build();
}

public ClientConfigResponse clientConfig(ClientConfigRequest request) {

if (!platformConfig.containsClientConfig(request.getApplication())) {
var message = String.format("Unknown client application: [%s]", request.getApplication());
log.error(message);
throw new EResourceNotFound(message);
}

var clientConfig = platformConfig.getClientConfigOrThrow(request.getApplication());

return ClientConfigResponse.newBuilder()
.putAllProperties(clientConfig.getPropertiesMap())
.build();
}

public ListResourcesResponse listResources(String tenantCode, ResourceType resourceType) {

// Explicit check is required because resources currently come from platform config
Expand All @@ -93,12 +105,12 @@ public ListResourcesResponse listResources(String tenantCode, ResourceType resou
var response = ListResourcesResponse.newBuilder();

if (resourceType == ResourceType.MODEL_REPOSITORY) {
for (var repoEntry : config.getRepositoriesMap().entrySet()) {
for (var repoEntry : platformConfig.getRepositoriesMap().entrySet()) {
response.addResources(buildResourceInfo(resourceType, repoEntry.getKey(), repoEntry.getValue()));
}
}
else if (resourceType == ResourceType.INTERNAL_STORAGE) {
for (var storageEntry : config.getStorage().getBucketsMap().entrySet()) {
for (var storageEntry : platformConfig.getStorage().getBucketsMap().entrySet()) {
response.addResources(buildResourceInfo(resourceType, storageEntry.getKey(), storageEntry.getValue()));
}
}
Expand All @@ -120,23 +132,23 @@ public ResourceInfoResponse resourceInfo(String tenantCode, ResourceType resourc

if (resourceType == ResourceType.MODEL_REPOSITORY) {

if (!this.config.containsRepositories(resourceKey)){
if (!this.platformConfig.containsRepositories(resourceKey)){
var message = String.format("Model repository not found: [%s]", resourceKey);
log.error(message);
throw new EResourceNotFound(message);
}

pluginConfig = this.config.getRepositoriesOrThrow(resourceKey);
pluginConfig = this.platformConfig.getRepositoriesOrThrow(resourceKey);
}
else if (resourceType == ResourceType.INTERNAL_STORAGE) {

if (!this.config.getStorage().containsBuckets(resourceKey)) {
if (!this.platformConfig.getStorage().containsBuckets(resourceKey)) {
var message = String.format("Storage location not found: [%s]", resourceKey);
log.error(message);
throw new EResourceNotFound(message);
}

pluginConfig = this.config.getStorage().getBucketsOrThrow(resourceKey);
pluginConfig = this.platformConfig.getStorage().getBucketsOrThrow(resourceKey);
}
else {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,40 @@ void getResource_invalidResourceType() {
Assertions.assertEquals(Status.Code.INVALID_ARGUMENT, e.getStatus().getCode());
}

@Test
void getClientConfig_ok() {

var request = ClientConfigRequest.newBuilder()
.setApplication("client-app")
.build();

var response = readApi.clientConfig(request);

Assertions.assertEquals(1, response.getPropertiesCount());
Assertions.assertTrue(response.containsProperties("unit.test.property"));
Assertions.assertEquals("value1", response.getPropertiesOrThrow("unit.test.property"));
}

@Test
void getClientConfig_appInvalid() {

var request = ClientConfigRequest.newBuilder()
.setApplication("%%%client-app")
.build();

var e = Assertions.assertThrows(StatusRuntimeException.class, () -> readApi.clientConfig(request));
Assertions.assertEquals(Status.Code.INVALID_ARGUMENT, e.getStatus().getCode());
}

@Test
void getClientConfig_appNotFound() {

var request = ClientConfigRequest.newBuilder()
.setApplication("unknown-app")
.build();

var e = Assertions.assertThrows(StatusRuntimeException.class, () -> readApi.clientConfig(request));
Assertions.assertEquals(Status.Code.NOT_FOUND, e.getStatus().getCode());
}

}

0 comments on commit a49728c

Please sign in to comment.