All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.finos.tracdap.test.helpers.PlatformTest Maven / Gradle / Ivy

Go to download

TRAC D.A.P. test library, pulls in everything needed to run tests across the TRAC platform services

There is a newer version: 0.6.3
Show newest version
/*
 * Copyright 2022 Accenture Global Solutions Limited
 *
 * 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
 *
 *     http://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.finos.tracdap.test.helpers;

import org.finos.tracdap.api.TracDataApiGrpc;
import org.finos.tracdap.api.TracMetadataApiGrpc;
import org.finos.tracdap.api.TracOrchestratorApiGrpc;
import org.finos.tracdap.api.internal.TrustedMetadataApiGrpc;
import org.finos.tracdap.common.auth.internal.ClientAuthProvider;
import org.finos.tracdap.common.auth.external.AuthLogic;
import org.finos.tracdap.common.auth.internal.JwtSetup;
import org.finos.tracdap.common.auth.internal.UserInfo;
import org.finos.tracdap.common.config.ConfigKeys;
import org.finos.tracdap.common.config.ConfigManager;
import org.finos.tracdap.common.plugin.PluginManager;
import org.finos.tracdap.common.startup.StandardArgs;
import org.finos.tracdap.common.util.RoutingUtils;
import org.finos.tracdap.config.PlatformConfig;
import org.finos.tracdap.tools.secrets.SecretTool;
import org.finos.tracdap.tools.deploy.metadb.DeployMetaDB;
import org.finos.tracdap.svc.data.TracDataService;
import org.finos.tracdap.svc.meta.TracMetadataService;
import org.finos.tracdap.svc.orch.TracOrchestratorService;
import org.finos.tracdap.gateway.TracPlatformGateway;
import org.finos.tracdap.test.config.ConfigHelpers;

import io.grpc.*;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.channel.nio.NioEventLoopGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;


public class PlatformTest implements BeforeAllCallback, AfterAllCallback {

    public static final String TRAC_EXEC_DIR = "TRAC_EXEC_DIR";
    public static final String STORAGE_ROOT_DIR = "storage_root";
    public static final String DEFAULT_STORAGE_FORMAT = "ARROW_FILE";

    private static final String SECRET_KEY_ENV_VAR = "TRAC_SECRET_KEY";
    private static final String SECRET_KEY_DEFAULT = "d7xbeK-julOi8-bBwd9k";

    private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows");
    private static final String PYTHON_EXE = IS_WINDOWS ? "python.exe" : "python";
    private static final String VENV_BIN_SUBDIR = IS_WINDOWS ? "Scripts" : "bin";
    private static final String VENV_ENV_VAR = "VIRTUAL_ENV";
    private static final String TRAC_RUNTIME_DIST_DIR = "tracdap-runtime/python/build/dist";

    private final Logger log = LoggerFactory.getLogger(getClass());

    private final String testConfig;
    private final List tenants;
    private final String storageFormat;

    private final boolean runDbDeploy;
    private final boolean manageDataPrefix;
    private final boolean localExecutor;

    private final boolean startMeta;
    private final boolean startData;
    private final boolean startOrch;
    private final boolean startGateway;

    private String authToken;

    private PlatformTest(
            String testConfig, List tenants, String storageFormat,
            boolean runDbDeploy, boolean manageDataPrefix, boolean localExecutor,
            boolean startMeta, boolean startData, boolean startOrch, boolean startGateway) {

        this.testConfig = testConfig;
        this.tenants = tenants;
        this.storageFormat = storageFormat;
        this.runDbDeploy = runDbDeploy;
        this.manageDataPrefix = manageDataPrefix;
        this.localExecutor = localExecutor;
        this.startMeta = startMeta;
        this.startData = startData;
        this.startOrch = startOrch;
        this.startGateway = startGateway;
    }

    public static Builder forConfig(String testConfig) {
        var builder = new Builder();
        builder.testConfig = testConfig;
        return builder;
    }

    public static class Builder {

        private String testConfig;
        private final List tenants = new ArrayList<>();
        private String storageFormat = DEFAULT_STORAGE_FORMAT;
        private boolean runDbDeploy = true;  // Run DB deploy by default
        private boolean manageDataPrefix = false;
        private boolean localExecutor = false;
        private boolean startMeta;
        private boolean startData;
        private boolean startOrch;
        private boolean startGateway;

        public Builder addTenant(String testTenant) { this.tenants.add(testTenant); return this; }
        public Builder storageFormat(String storageFormat) { this.storageFormat = storageFormat; return this; }
        public Builder runDbDeploy(boolean runDbDeploy) { this.runDbDeploy = runDbDeploy; return this; }
        public Builder manageDataPrefix(boolean manageDataPrefix) { this.manageDataPrefix = manageDataPrefix; return this; }
        public Builder prepareLocalExecutor(boolean localExecutor) { this.localExecutor = localExecutor; return this; }
        public Builder startMeta() { startMeta = true; return this; }
        public Builder startData() { startData = true; return this; }
        public Builder startOrch() { startOrch = true; return this; }
        public Builder startGateway() { startGateway = true; return this; }
        public Builder startAll() { return startMeta().startData().startOrch().startGateway(); }

        public PlatformTest build() {

            return new PlatformTest(
                    testConfig, tenants, storageFormat,
                    runDbDeploy, manageDataPrefix, localExecutor,
                    startMeta, startData, startOrch, startGateway);
        }
    }

    private String testId;
    private Path tracDir;
    private Path tracStorageDir;
    private Path tracExecDir;    private Path tracRepoDir;
    private URL platformConfigUrl;
    private String secretKey;
    private PlatformConfig platformConfig;

    private TracMetadataService metaSvc;
    private TracDataService dataSvc;
    private TracOrchestratorService orchSvc;
    private TracPlatformGateway gatewaySvc;

    private ManagedChannel metaChannel;
    private ManagedChannel dataChannel;
    private ManagedChannel orchChannel;

    public TracMetadataApiGrpc.TracMetadataApiFutureStub metaClientFuture() {
        var client = TracMetadataApiGrpc.newFutureStub(metaChannel);
        return ClientAuthProvider.applyIfAvailable(client, authToken);
    }

    public TracMetadataApiGrpc.TracMetadataApiBlockingStub metaClientBlocking() {
        var client = TracMetadataApiGrpc.newBlockingStub(metaChannel);
        return ClientAuthProvider.applyIfAvailable(client, authToken);
    }

    public TrustedMetadataApiGrpc.TrustedMetadataApiBlockingStub metaClientTrustedBlocking() {
        var client = TrustedMetadataApiGrpc.newBlockingStub(metaChannel);
        return ClientAuthProvider.applyIfAvailable(client, authToken);
    }

    public TracDataApiGrpc.TracDataApiStub dataClient() {
        var client = TracDataApiGrpc.newStub(dataChannel);
        return ClientAuthProvider.applyIfAvailable(client, authToken);
    }

    public TracDataApiGrpc.TracDataApiBlockingStub dataClientBlocking() {
        var client = TracDataApiGrpc.newBlockingStub(dataChannel);
        return ClientAuthProvider.applyIfAvailable(client, authToken);
    }

    public TracOrchestratorApiGrpc.TracOrchestratorApiBlockingStub orchClientBlocking() {
        var client = TracOrchestratorApiGrpc.newBlockingStub(orchChannel);
        return ClientAuthProvider.applyIfAvailable(client, authToken);
    }

    public String platformConfigUrl() {
        return platformConfigUrl.toString();
    }

    public Path tracDir() {
        return tracDir;
    }

    public Path tracRepoDir() {
        return tracRepoDir;
    }

    @Override
    public void beforeAll(ExtensionContext context) throws Exception {

        setTestId();

        findDirectories();
        prepareConfig();
        preparePlugins();

        prepareAuth();

        if (runDbDeploy)
            prepareDatabase();

        if (manageDataPrefix)
            prepareDataPrefix();

        startServices();
        startClients();
    }

    @Override
    public void afterAll(ExtensionContext context) throws Exception {

        stopClients();
        stopServices();

        if (manageDataPrefix)
            cleanupDataPrefix();

        cleanupDirectories();
    }

    void setTestId() {

        var timestamp = DateTimeFormatter.ISO_INSTANT.format(Instant.now()).replace(':', '.');
        var random = new Random().nextLong();

        testId = String.format("%s_0x%h", timestamp, random);
    }

    void findDirectories() throws Exception {

        tracDir = Files.createTempDirectory("trac_platform_test_");

        tracStorageDir = tracDir.resolve(STORAGE_ROOT_DIR);
        Files.createDirectory(tracStorageDir);

        tracExecDir = System.getenv().containsKey(TRAC_EXEC_DIR)
                ? Paths.get(System.getenv(TRAC_EXEC_DIR))
                : tracDir;

        tracRepoDir = Paths.get(".").toAbsolutePath();

        while (!Files.exists(tracRepoDir.resolve("tracdap-api")))
            tracRepoDir = tracRepoDir.getParent();
    }

    void cleanupDirectories() {

        try (var walk = Files.walk(tracDir)) {

            walk.sorted(Comparator.reverseOrder())
                    .map(Path::toFile)
                    .forEach(File::delete);
        }
        catch (IOException e) {

            log.warn("Failed to clean up test files: " + e.getMessage(), e);
        }
    }

    void prepareConfig() throws Exception {

        log.info("Prepare config for platform testing...");

        // Git is not available in CI for tests run inside containers
        // So, only look up the current repo if it is needed by the orchestrator
        // To run orchestrator tests in a container, we'd need to pass the repo URL in, e.g. with an env var from CI
        String currentGitOrigin = localExecutor
                ? getCurrentGitOrigin()
                : "git_repo_not_configured";

        // Substitutions are used by template config files in test resources
        // But it is harmless to apply them to fully defined config files as well

        // The substitutions have some special handling in PlatformTest to set them up
        var staticSubstitutions = Map.of(
                "${TRAC_DIR}", tracDir.toString().replace("\\", "\\\\"),
                "${TRAC_STORAGE_DIR}", tracStorageDir.toString().replace("\\", "\\\\"),
                "${TRAC_STORAGE_FORMAT}", storageFormat,
                "${TRAC_EXEC_DIR}", tracExecDir.toString().replace("\\", "\\\\"),
                "${TRAC_LOCAL_REPO}", tracRepoDir.toString(),
                "${TRAC_GIT_REPO}", currentGitOrigin,
                "${TRAC_TEST_ID}", testId);

        var substitutions = new HashMap<>(staticSubstitutions);

        // Also allow developers to put whatever substitutions they need into the environment
        for (var envVar : System.getenv().entrySet()) {
            if (envVar.getKey().startsWith("TRAC_") && !substitutions.containsKey(envVar.getKey())) {
                var key = String.format("${%s}", envVar.getKey());
                substitutions.put(key, envVar.getValue());
            }
        }

        var configInputs = List.of(testConfig);
        var configOutputs = ConfigHelpers.prepareConfig(configInputs, tracDir, substitutions);
        platformConfigUrl = configOutputs.get(0);

        // The Secret key is used for storing and accessing secrets
        // If secrets are set up externally, a key can be passed in the env to access the secret store
        // Otherwise the default is used, which is fine if the store is being initialised here
        var env = System.getenv();
        secretKey = env.getOrDefault(SECRET_KEY_ENV_VAR, SECRET_KEY_DEFAULT);

        var plugins = new PluginManager();
        plugins.initConfigPlugins();

        var config = new ConfigManager(platformConfigUrl.toString(), tracDir, plugins);
        platformConfig = config.loadRootConfigObject(PlatformConfig.class);
    }

    private String getCurrentGitOrigin() throws Exception {

        var pb = new ProcessBuilder();
        pb.command("git", "config", "--get", "remote.origin.url");

        var proc = pb.start();

        try {
            proc.waitFor(10, TimeUnit.SECONDS);

            var procResult = proc.getInputStream().readAllBytes();
            var origin = new String(procResult, StandardCharsets.UTF_8).strip();

            log.info("Using Git origin: {}", origin);

            return origin;
        }
        finally {
            proc.destroy();
        }
    }

    void prepareAuth() throws URISyntaxException {

        log.info("Running auth tool to set up root authentication keys...");

        // Running the auth tool will create the secrets file and add the public / private keys for auth

        var authTasks = new ArrayList();
        authTasks.add(StandardArgs.task(SecretTool.CREATE_ROOT_AUTH_KEY, List.of("EC", "256"), ""));
        ServiceHelpers.runAuthTool(tracDir, platformConfigUrl, secretKey, authTasks);

        // Authentication is mandatory, so we need to build a token in order to test at the API level
        // To create a valid token, we need to get the auth signing keys out of the secrets file
        // Tokens must be signed with the same key used by the platform services

        var pluginMgr = new PluginManager();
        pluginMgr.initConfigPlugins();

        var configMgr = new ConfigManager(
                platformConfigUrl.toString(),
                Paths.get(platformConfigUrl.toURI()).getParent(),
                pluginMgr, secretKey);

        configMgr.prepareSecrets();

        var platformConfig = configMgr.loadRootConfigObject(PlatformConfig.class);
        var authConfig = platformConfig.getAuthentication();

        var jwt = JwtSetup.createProcessor(platformConfig, configMgr);

        var userInfo = new UserInfo();
        userInfo.setUserId("platform_testing");
        userInfo.setDisplayName("Platform testing user");

        var session = AuthLogic.newSession(userInfo, authConfig);

        authToken = jwt.encodeToken(session);
    }

    void prepareDatabase() {

        log.info("Deploy database schema...");

        var databaseTasks = new ArrayList();
        databaseTasks.add(StandardArgs.task(DeployMetaDB.DEPLOY_SCHEMA_TASK, "", ""));

        for (var tenant : tenants) {

            // Run both add and alter tenant tasks as part of the standard setup
            // (just to run both tasks, not strictly necessary)

            var description = "Test tenant [" + tenant + "]";
            databaseTasks.add(StandardArgs.task(DeployMetaDB.ADD_TENANT_TASK, List.of(tenant, description), ""));
            databaseTasks.add(StandardArgs.task(DeployMetaDB.ALTER_TENANT_TASK, List.of(tenant, description), ""));
        }

        ServiceHelpers.runDbDeploy(tracDir, platformConfigUrl, secretKey, databaseTasks);
    }

    void prepareDataPrefix() throws Exception {

        var elg = new NioEventLoopGroup(2);

        var plugins = new PluginManager();
        plugins.initConfigPlugins();
        plugins.initRegularPlugins();

        var config = new ConfigManager(
                platformConfigUrl(),
                tracDir(),
                plugins);

        StorageTestHelpers.createStoragePrefix(config, plugins, elg);

        elg.shutdownGracefully();
    }

    void cleanupDataPrefix() throws Exception {

        var elg = new NioEventLoopGroup(2);

        var plugins = new PluginManager();
        plugins.initConfigPlugins();
        plugins.initRegularPlugins();

        var config = new ConfigManager(
                platformConfigUrl(),
                tracDir(),
                plugins);

        StorageTestHelpers.deleteStoragePrefix(config, plugins, elg);

        elg.shutdownGracefully();
    }

    void preparePlugins() throws Exception {

        // TODO: Allow running whole-platform tests over different backend configurations

        if (startData) {
            Files.createDirectory(tracDir.resolve("unit_test_storage"));
        }

        if (startOrch && localExecutor) {

            var venvDir = tracExecDir.resolve("venv").normalize();

            if (Files.exists(venvDir)) {

                log.info("Using existing venv: [{}]", venvDir);
            }
            else {
                log.info("Creating a new venv: [{}]", venvDir);

                var venvPath = tracDir.resolve("venv");
                var venvPb = new ProcessBuilder();
                venvPb.command("python", "-m", "venv", venvPath.toString());

                var venvP = venvPb.start();
                venvP.waitFor(60, TimeUnit.SECONDS);

                log.info("Installing TRAC runtime for Python...");

                // This assumes the runtime has already been built externally (requires full the Python build chain)

                var pythonExe = venvPath
                        .resolve(VENV_BIN_SUBDIR)
                        .resolve(PYTHON_EXE)
                        .toString();

                var tracRtDistDir = tracRepoDir.resolve(TRAC_RUNTIME_DIST_DIR);
                var tracRtWhl = Files.find(tracRtDistDir, 1, (file, attrs) -> file.toString().endsWith(".whl"))
                        .findFirst();

                if (tracRtWhl.isEmpty())
                    throw new RuntimeException("Could not find TRAC runtime wheel");

                var pipPB = new ProcessBuilder();
                pipPB.command(pythonExe, "-m", "pip", "install", tracRtWhl.get().toString());
                pipPB.environment().put(VENV_ENV_VAR, venvPath.toString());

                var pipP = pipPB.start();
                pipP.waitFor(2, TimeUnit.MINUTES);

            }
        }
    }

    void startServices() {

        if (startMeta)
            metaSvc = ServiceHelpers.startService(TracMetadataService.class, tracDir, platformConfigUrl, secretKey);

        if (startData)
            dataSvc = ServiceHelpers.startService(TracDataService.class, tracDir, platformConfigUrl, secretKey);

        if (startOrch)
            orchSvc = ServiceHelpers.startService(TracOrchestratorService.class, tracDir, platformConfigUrl, secretKey);

        if (startGateway)
            gatewaySvc = ServiceHelpers.startService(TracPlatformGateway.class, tracDir, platformConfigUrl, secretKey);
    }

    void stopServices() {

        if (gatewaySvc != null)
            gatewaySvc.stop();

        if (orchSvc != null)
            orchSvc.stop();

        if (dataSvc != null)
            dataSvc.stop();

        if (metaSvc != null)
            metaSvc.stop();
    }

    void startClients() {

        if (startMeta)
            metaChannel = channelForService(platformConfig, ConfigKeys.METADATA_SERVICE_KEY);

        if (startData)
            dataChannel = channelForService(platformConfig, ConfigKeys.DATA_SERVICE_KEY);

        if (startOrch)
            orchChannel = channelForService(platformConfig, ConfigKeys.ORCHESTRATOR_SERVICE_KEY);
    }

    void stopClients() throws Exception {

        if (orchChannel != null)
            orchChannel.shutdown().awaitTermination(10, TimeUnit.SECONDS);

        if (dataChannel != null)
            dataChannel.shutdown().awaitTermination(10, TimeUnit.SECONDS);

        if (metaChannel != null)
            metaChannel.shutdown().awaitTermination(10, TimeUnit.SECONDS);
    }

    ManagedChannel channelForService(PlatformConfig platformConfig, String serviceKey) {

        var serviceTarget = RoutingUtils.serviceTarget(platformConfig, serviceKey);

        var builder = NettyChannelBuilder.forAddress(serviceTarget.getHost(), serviceTarget.getPort());
        // Tests run on basic HTTP for now
        builder.usePlaintext();
        builder.directExecutor();

        return builder.build();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy