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

io.bdeploy.jersey.cli.RemoteServiceTool Maven / Gradle / Ivy

Go to download

Public API including dependencies, ready to be used for integrations and plugins.

There is a newer version: 7.3.6
Show newest version
package io.bdeploy.jersey.cli;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import io.bdeploy.common.ActivityReporter;
import io.bdeploy.common.ActivitySnapshot;
import io.bdeploy.common.NoThrowAutoCloseable;
import io.bdeploy.common.cfg.Configuration.EnvironmentFallback;
import io.bdeploy.common.cfg.Configuration.Help;
import io.bdeploy.common.cfg.Configuration.Validator;
import io.bdeploy.common.cfg.ExistingPathValidator;
import io.bdeploy.common.cfg.RemoteValidator;
import io.bdeploy.common.cli.ToolBase;
import io.bdeploy.common.cli.ToolBase.ConfiguredCliTool;
import io.bdeploy.common.cli.data.RenderableResult;
import io.bdeploy.common.security.OnDiscKeyStore;
import io.bdeploy.common.security.RemoteService;
import io.bdeploy.common.util.JacksonHelper;
import io.bdeploy.jersey.JerseyClientFactory;
import io.bdeploy.jersey.activity.JerseyBroadcastingActivityReporter;
import io.bdeploy.jersey.ws.change.client.ObjectChangeClientWebSocket;
import io.bdeploy.jersey.ws.change.msg.ObjectScope;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.core.UriBuilder;

/**
 * Base class for all CLI tools which need to access a remote service. Figuring
 * out the {@link RemoteService} based on command line arguments is don entirely
 * here.
 */
public abstract class RemoteServiceTool extends ConfiguredCliTool {

    @Help("Configuration for remote access")
    private @interface RemoteConfig {

        @Help("URI of remote BHive. Supports file:, jar:file:")
        @EnvironmentFallback("BDEPLOY_REMOTE")
        @Validator(RemoteValidator.class)
        String remote();

        @Help("Path to keystore containing access token.")
        @Validator(ExistingPathValidator.class)
        String keystore();

        @Help("Passphrase for the keystore.")
        String passphrase();

        @Help("Token for the remote access. Can be given alternatively to a keystore.")
        @EnvironmentFallback("BDEPLOY_TOKEN")
        String token();

        @Help("Path to a file containing the access token. Can be given alternatively to a keystore.")
        @EnvironmentFallback("BDEPLOY_TOKENFILE")
        @Validator(ExistingPathValidator.class)
        String tokenFile();

        @Help("Override which named login session to use for this command.")
        @EnvironmentFallback("BDEPLOY_LOGIN")
        String useLogin();
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    protected @interface RemoteOptional {
    }

    protected RemoteServiceTool(Class configClass) {
        super(configClass);
    }

    @Override
    protected List> getConfigsForHelp() {
        List> help = new ArrayList<>();
        help.add(RemoteConfig.class);
        help.addAll(super.getConfigsForHelp());
        return help;
    }

    private boolean isOptional() {
        try {
            Method method = getClass().getDeclaredMethod("run", getPrimaryConfigClass(), RemoteService.class);
            return method.getParameters()[1].getAnnotation(RemoteOptional.class) != null;
        } catch (NoSuchMethodException e) {
            out().println("WARN: cannot determine whether remote is optional: " + e.toString());
            return false;
        }
    }

    @Override
    protected final RenderableResult run(T config) {
        RemoteConfig rc = getConfig(RemoteConfig.class);
        boolean optional = isOptional();

        boolean isTestMode = ToolBase.isTestModeLLM();
        LocalLoginManager llm = new LocalLoginManager();

        if (!optional && (!isTestMode && (llm.getCurrent() == null && rc.useLogin() == null))) {
            helpAndFailIfMissing(rc.remote(), "Missing --remote, --useLogin, or current login using `bdeploy login`");
        }

        RemoteService svc = null;
        if (rc.remote() != null) {
            svc = createServiceFromCli(rc, optional);
        } else if (!isTestMode) {
            svc = createServiceFromLLM(rc, optional, llm);
        }

        if (getActivityReporter() instanceof ActivityReporter.Stream) {
            ((ActivityReporter.Stream) getActivityReporter()).setProxyConnector(this::connectProxy);
        }

        JerseyClientFactory.setDefaultReporter(getActivityReporter());

        try (NoThrowAutoCloseable proxy = getActivityReporter().proxyActivities(svc)) {
            return run(config, svc);
        }
    }

    protected SecurityContext getLocalContext() {
        return null; // always null for the CLI. Ther server will infer the context from the token;
    }

    private RemoteService createServiceFromLLM(RemoteConfig rc, boolean optional, LocalLoginManager llm) {
        RemoteService svc;
        if (rc.useLogin() != null) {
            svc = llm.getNamedService(rc.useLogin());
        } else {
            svc = llm.getCurrentService();
        }
        if (!optional && svc == null) {
            helpAndFail(
                    "Need either --tokenFile, --token or --keystore arguments or a current login session to access remote service");
        }
        return svc;
    }

    private RemoteService createServiceFromCli(RemoteConfig rc, boolean optional) {
        RemoteService svc;
        URI r = null;
        if (rc.remote() != null) {
            r = UriBuilder.fromUri(rc.remote()).build();
        }

        if (rc.tokenFile() != null && rc.token() != null) {
            out().println(
                    "WARNING: both tokenFile and token are given, preferring tokenFile (Hint: check arguments and environment)");
        }

        svc = createRemoteService(rc, optional, r);
        return svc;
    }

    private NoThrowAutoCloseable connectProxy(RemoteService remote, Consumer> onMessage) {
        return new NoThrowAutoCloseable() {

            private ObjectChangeClientWebSocket ws;

            {
                try {
                    this.ws = JerseyClientFactory.get(remote).getObjectChangeWebSocket(c -> {
                        String serialized = c.details.get(JerseyBroadcastingActivityReporter.OCT_ACTIVIES);
                        try {
                            onMessage.accept(
                                    JacksonHelper.getDefaultJsonObjectMapper().readValue(serialized, ActivitySnapshot.LIST_TYPE));
                        } catch (Exception e) {
                            out().println("Cannot read remote activities");
                            e.printStackTrace(out());
                        }
                    });
                    this.ws.subscribe(JerseyBroadcastingActivityReporter.OCT_ACTIVIES, ObjectScope.EMPTY);
                } catch (Exception e) {
                    out().println("Cannot initialize Acitivities WebSocket");
                }
            }

            @Override
            public void close() {
                if (ws != null) {
                    ws.close();
                }
            }
        };
    }

    private RemoteService createRemoteService(RemoteConfig rc, boolean optional, URI r) {
        RemoteService svc = null;
        if (rc.tokenFile() != null) {
            try {
                String token = new String(Files.readAllBytes(Paths.get(rc.tokenFile())), StandardCharsets.UTF_8);
                svc = new RemoteService(r, token);
            } catch (IOException e) {
                throw new IllegalStateException("Cannot read token from " + rc.tokenFile(), e);
            }
        } else if (rc.token() != null) {
            svc = new RemoteService(r, rc.token());
        } else if (rc.keystore() != null) {
            helpAndFailIfMissing(rc.passphrase(), "Missing --passphrase");

            svc = new RemoteService(r, new OnDiscKeyStore(Paths.get(rc.keystore()), rc.passphrase()));
        } else if (r != null && (r.getScheme() == null || !r.getScheme().equals("https"))) {
            if (r.getScheme() == null) {
                svc = new RemoteService(UriBuilder.fromUri(r).scheme("file").build());
            } else {
                svc = new RemoteService(UriBuilder.fromUri(r).build());
            }
        } else {
            if (!optional) {
                helpAndFail("Need either --tokenFile, --token or --keystore arguments to access remote service");
            }
        }

        return svc;
    }

    protected abstract RenderableResult run(T config, RemoteService remote);

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy