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

se.arkalix.core.plugin.HttpJsonCloudPlugin Maven / Gradle / Ivy

package se.arkalix.core.plugin;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.arkalix.ArService;
import se.arkalix.ArSystem;
import se.arkalix.core.plugin.dto.*;
import se.arkalix.description.ProviderDescription;
import se.arkalix.description.ServiceDescription;
import se.arkalix.descriptor.EncodingDescriptor;
import se.arkalix.descriptor.InterfaceDescriptor;
import se.arkalix.internal.security.identity.X509Keys;
import se.arkalix.net.http.client.HttpClient;
import se.arkalix.net.http.consumer.HttpConsumer;
import se.arkalix.plugin.Plug;
import se.arkalix.plugin.Plugin;
import se.arkalix.query.ServiceQuery;
import se.arkalix.security.access.AccessByToken;
import se.arkalix.security.identity.SystemIdentity;
import se.arkalix.security.identity.UnsupportedKeyAlgorithm;
import se.arkalix.util.Result;
import se.arkalix.util.concurrent.Future;
import se.arkalix.util.concurrent.FutureAnnouncement;

import java.net.InetSocketAddress;
import java.security.PublicKey;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Objects;
import java.util.stream.Collectors;

import static se.arkalix.descriptor.SecurityDescriptor.CERTIFICATE;
import static se.arkalix.descriptor.SecurityDescriptor.NOT_SECURE;
import static se.arkalix.descriptor.TransportDescriptor.HTTP;

/**
 * HTTP/JSON cloud plugin.
 * 

* This class helps one or more {@link se.arkalix.ArSystem systems} with * joining a local cloud by communicating with the mandatory Arrowhead core * services of that cloud using HTTP and JSON. More precisely, it (1) registers * and unregisters the {@link se.arkalix.ArSystem#provide(ArService) services * provided} by its systems, (2) retrieves the public key required to {@link * se.arkalix.security.access.AccessByToken validate consumer tokens}, as well * as (3) helps resolve {@link se.arkalix.ArSystem#consume() service * consumption queries}. *

* If used, this plugin should typically be the first one in the list of * plugins provided to {@link se.arkalix.ArSystem.Builder#plugins(Plugin...) * constructed systems}, as subsequent plugins may depend on it being * available for service resolution when they attach to the system in question. *

* Note that the plugin currently assumes that the service registry, * authorization system public key and orchestrator of the cloud in question * will never change. It also never adds the service discovery or orchestration * services to the {@link ArSystem#consumedServices() service caches} of the * systems using them, making them inaccessible via the * {@link ArSystem#consume()} method. */ public class HttpJsonCloudPlugin implements Plugin { private static final Logger logger = LoggerFactory.getLogger(HttpJsonCloudPlugin.class); private final InetSocketAddress serviceRegistrySocketAddress; private final Object serviceDiscoveryLock = new Object(); private final String serviceDiscoveryBasePath; private FutureAnnouncement serviceDiscoveryAnnouncement = null; private final Object orchestrationLock = new Object(); private final ArOrchestrationStrategy orchestrationStrategy; private FutureAnnouncement orchestrationAnnouncement = null; private HttpClient client = null; private final Object authorizationKeyLock = new Object(); private FutureAnnouncement authorizationKeyAnnouncement = null; private HttpJsonCloudPlugin(final Builder builder) { serviceDiscoveryBasePath = Objects.requireNonNullElse(builder.serviceDiscoveryBasePath, "/serviceregistry"); serviceRegistrySocketAddress = Objects.requireNonNull(builder.serviceRegistrySocketAddress, "Expected serviceRegistrySocketAddress"); orchestrationStrategy = Objects.requireNonNullElse(builder.orchestrationStrategy, ArOrchestrationStrategy.STORED_THEN_DYNAMIC); } /** * Creates new HTTP/JSON cloud plugin that tries to make its systems enter * a certain local cloud via the service registry system available at the * specified {@code socketAddress}. *

* If more control over the behavior of the core integrator is desired, * please use the {@link Builder builder class} instead. * * @param socketAddress IP address or hostname and port of service registry * system to use for entering local cloud. * @return New core integrator. */ public static HttpJsonCloudPlugin viaServiceRegistryAt(final InetSocketAddress socketAddress) { return new HttpJsonCloudPlugin.Builder() .serviceRegistrySocketAddress(socketAddress) .build(); } @Override public void onAttach(final Plug plug) throws Exception { client = HttpClient.from(plug.system()); if (logger.isInfoEnabled()) { logger.info("HTTP/JSON cloud plugin attached to \"{}\"", plug.system().name()); } } @Override public void onDetach(final Plug plug) { if (logger.isInfoEnabled()) { logger.info("HTTP/JSON cloud plugin detached from \"{}\"", plug.system().name()); } } @Override public void onDetach(final Plug plug, final Throwable cause) { if (logger.isErrorEnabled()) { logger.error("HTTP/JSON cloud plugin forcibly detached " + "from \"" + plug.system().name() + "\"", cause); } } @Override public Future onServicePrepared(final Plug plug, final ArService service) { final var accessPolicy = service.accessPolicy(); if (accessPolicy instanceof AccessByToken) { return requestAuthorizationKey() .ifSuccess(((AccessByToken) accessPolicy)::authorizationKey); } return Future.done(); } @Override public Future onServiceProvided(final Plug plug, final ServiceDescription service) { if (logger.isInfoEnabled()) { logger.info("Registering \"{}\" provided by \"{}\" ...", service.name(), plug.system().name()); } final var provider = service.provider(); final var providerSocketAddress = provider.socketAddress(); final var registration = ServiceRegistration.from(service); return requestServiceDiscovery() .flatMap(serviceDiscovery -> serviceDiscovery .register(registration) .flatMapCatch(ErrorException.class, fault -> { final var error = fault.error(); if ("INVALID_PARAMETER".equals(error.type())) { return serviceDiscovery.unregister( service.name(), provider.name(), providerSocketAddress.getHostString(), providerSocketAddress.getPort()) .flatMap(ignored -> serviceDiscovery.register(registration) .pass(null)); } return Future.failure(fault); }) .mapResult(result -> { if (result.isSuccess()) { if (logger.isInfoEnabled()) { logger.info("Registered the \"{}\" service " + "provided by the \"{}\" system", service.name(), plug.system().name()); } } else { if (logger.isErrorEnabled()) { logger.error("Failed to register the \"" + service.name() + "\" service provided by the \"" + plug.system().name() + "\" system", result.fault()); } } return result; })); } @Override public void onServiceDismissed(final Plug plug, final ServiceDescription service) { if (logger.isInfoEnabled()) { logger.info("Unregistering the \"{}\" service provided by " + "the \"{}\" system ...", service.name(), plug.system().name()); } final var provider = service.provider(); final var providerSocketAddress = provider.socketAddress(); requestServiceDiscovery() .flatMap(serviceDiscovery -> serviceDiscovery.unregister( service.name(), provider.name(), providerSocketAddress.getAddress().getHostAddress(), providerSocketAddress.getPort())) .onResult(result -> { if (result.isSuccess()) { if (logger.isInfoEnabled()) { logger.info("Unregistered the \"{}\" service " + "provided by the \"{}\" system", service.name(), plug.system().name()); } } else { if (logger.isWarnEnabled()) { logger.warn("Failed to unregister the \"" + service.name() + "\" service provided by the \"" + plug.system().name() + "\" system", result.fault()); } } }); } @Override public Future> onServiceQueried(final Plug plug, final ServiceQuery query) { final var system = plug.system(); switch (orchestrationStrategy) { case STORED_ONLY: return queryOrchestratorForStoredRules(system); case STORED_THEN_DYNAMIC: return queryOrchestratorForStoredRules(system) .flatMap(services -> { if (services.stream().anyMatch(query::matches)) { return Future.success(services); } return queryOrchestratorForDynamicRules(system, query); }); case DYNAMIC_ONLY: return queryOrchestratorForDynamicRules(system, query); } throw new IllegalStateException("Unsupported orchestration strategy: " + orchestrationStrategy); } private Future requestServiceDiscovery() { synchronized (serviceDiscoveryLock) { if (serviceDiscoveryAnnouncement == null) { if (logger.isInfoEnabled()) { logger.info("HTTP/JSON cloud plugin connecting to " + "\"service_registry\" system at {} ...", serviceRegistrySocketAddress); } serviceDiscoveryAnnouncement = client.connect(serviceRegistrySocketAddress) .mapResult(result -> { if (result.isFailure()) { return Result.failure(result.fault()); } final var connection = result.value(); final var isSecure = connection.isSecure(); final ProviderDescription provider; if (isSecure) { final var identity = new SystemIdentity(connection.certificateChain()); final var name = identity.name(); if (!Objects.equals(name, "service_registry")) { return Result.failure(new ArCoreIntegrationException("" + "HTTP/JSON cloud plugin connected to " + "system at " + serviceRegistrySocketAddress + " and found that its certificate name " + "is \"" + name + "\" while expecting it " + "to be \"service_registry\"; failed to " + "resolve service discovery service ")); } provider = new ProviderDescription(name, serviceRegistrySocketAddress, identity.publicKey()); } else { provider = new ProviderDescription("service_registry", serviceRegistrySocketAddress); } final var serviceDiscovery = new HttpJsonServiceDiscovery(client, new ServiceDescription.Builder() .name("service-discovery") .provider(provider) .uri(serviceDiscoveryBasePath) .security(isSecure ? CERTIFICATE : NOT_SECURE) .interfaces(InterfaceDescriptor.getOrCreate(HTTP, isSecure, EncodingDescriptor.JSON)) .build()); connection.close(); if (logger.isInfoEnabled()) { logger.info("HTTP/JSON cloud plugin " + "connected to \"service_registry\" system " + "at {}", serviceRegistrySocketAddress); } return Result.success(serviceDiscovery); }) .ifFailure(Throwable.class, fault -> { if (logger.isErrorEnabled()) { logger.error("HTTP/JSON cloud plugin failed to " + "connect to \"service_registry\" system at " + serviceRegistrySocketAddress, fault); } }) .toAnnouncement(); } return serviceDiscoveryAnnouncement.subscribe(); } } private Future requestAuthorizationKey() { synchronized (authorizationKeyLock) { if (authorizationKeyAnnouncement == null) { if (logger.isInfoEnabled()) { logger.info("HTTP/JSON cloud plugin requesting authorization key ..."); } authorizationKeyAnnouncement = requestServiceDiscovery() .flatMap(serviceDiscovery -> serviceDiscovery.query(new ServiceQueryBuilder() .name("auth-public-key") .build())) .mapResult(result -> { if (result.isFailure()) { return Result.failure(result.fault()); } final var services = result.value().services(); if (services.size() == 0) { return Result.failure(new ArCoreIntegrationException("" + "No \"auth-public-key\" service seems to be " + "available via the service registry at: " + serviceRegistrySocketAddress + "; token " + "authorization not possible")); } String publicKeyBase64 = null; for (final var service : services) { final var key = service.provider().publicKeyBase64(); if (key.isPresent()) { publicKeyBase64 = key.get(); break; } } if (publicKeyBase64 == null) { return Result.failure(new ArCoreIntegrationException("" + "Even though the service registry provided " + "descriptions for " + services.size() + " " + "\"auth-public-key\" service(s), none of them " + "contains an authorization system public key; " + "token authorization not possible")); } final PublicKey publicKey; try { publicKey = X509Keys.parsePublicKey(publicKeyBase64); } catch (final UnsupportedKeyAlgorithm exception) { return Result.failure(new ArCoreIntegrationException("" + "The \"auth-public-key\" service provider public " + "key seems to use an unsupported key algorithm; " + "token authorization not possible", exception)); } if (logger.isInfoEnabled()) { logger.info("Authorization key retrieved: {}", publicKeyBase64); } return Result.success(publicKey); }) .ifFailure(Throwable.class, fault -> { if (logger.isWarnEnabled()) { logger.warn("Failed to retrieve authorization key", fault); } }) .toAnnouncement(); } return authorizationKeyAnnouncement.subscribe(); } } private Future requestOrchestration() { synchronized (orchestrationLock) { if (orchestrationAnnouncement == null) { if (logger.isInfoEnabled()) { logger.info("HTTP/JSON cloud plugin connecting to " + "\"orchestrator\" system ..."); } final var isSecure = client.isSecure(); orchestrationAnnouncement = requestServiceDiscovery() .flatMap(serviceDiscovery -> serviceDiscovery.query(new ServiceQueryBuilder() .name("orchestration-service") .interfaces(InterfaceDescriptor.getOrCreate(HTTP, isSecure, EncodingDescriptor.JSON)) .securityModes(isSecure ? CERTIFICATE : NOT_SECURE) .build())) .flatMapResult(result -> { if (result.isFailure()) { return Future.failure(result.fault()); } final var queryResult = result.value(); final var services = queryResult.services(); if (services.isEmpty()) { return Future.failure(new ArCoreIntegrationException("" + "No orchestration service available; cannot " + "request orchestration rules")); } final var orchestration = new HttpJsonOrchestration(new HttpConsumer( client, services.get(0).toServiceDescription(), Collections.singleton(EncodingDescriptor.JSON))); if (logger.isInfoEnabled()) { logger.info("Orchestration service resolved at {}", orchestration.service().provider().socketAddress()); } return Future.success(orchestration); }) .ifFailure(Throwable.class, fault -> { if (logger.isErrorEnabled()) { logger.error("HTTP/JSON cloud plugin failed " + "to connect to \"orchestrator\" system", fault); } }) .toAnnouncement(); } return orchestrationAnnouncement.subscribe(); } } private Future> queryOrchestratorForDynamicRules( final ArSystem system, final ServiceQuery query) { final var options = new HashMap(); options.put(OrchestrationOption.OVERRIDE_STORE, true); if (!query.metadata().isEmpty()) { options.put(OrchestrationOption.METADATA_SEARCH, true); } return requestOrchestration() .flatMap(orchestration -> orchestration.query(new OrchestrationQueryBuilder() .requester(SystemDetails.from(system)) .service(se.arkalix.core.plugin.dto.ServiceQuery.from(query)) .options(options) .build())) .map(queryResult -> queryResult.services() .stream() .map(ServiceConsumable::toServiceDescription) .collect(Collectors.toUnmodifiableList())); } private Future> queryOrchestratorForStoredRules(final ArSystem system) { return requestOrchestration() .flatMap(orchestration -> orchestration.query(new OrchestrationQueryBuilder() .requester(SystemDetails.from(system)) .build())) .map(queryResult -> queryResult.services() .stream() .map(ServiceConsumable::toServiceDescription) .collect(Collectors.toUnmodifiableList())); } /** * Builder useful for constructing {@link HttpJsonCloudPlugin} instances. */ @SuppressWarnings("unused") public static class Builder { private String serviceDiscoveryBasePath; private InetSocketAddress serviceRegistrySocketAddress; private ArOrchestrationStrategy orchestrationStrategy; /** * Sets base path, or service URI, of the service discovery * service provided by {@link * #serviceRegistrySocketAddress(InetSocketAddress)} the designated * service registry system}. If not specified, a default that should * work with most service registries will be used. * * @param serviceDiscoveryBasePath Base path of service discovery * service. * @return This builder. */ public Builder serviceDiscoveryBasePath(final String serviceDiscoveryBasePath) { this.serviceDiscoveryBasePath = serviceDiscoveryBasePath; return this; } /** * Sets hostname/IP-address and port of the service registry system to * use for entering into an Arrowhead local cloud. Must be * specified. * * @param serviceRegistrySocketAddress Service registry system socket * address. * @return This builder. */ public Builder serviceRegistrySocketAddress(final InetSocketAddress serviceRegistrySocketAddress) { this.serviceRegistrySocketAddress = serviceRegistrySocketAddress; return this; } /** * Sets {@link ArOrchestrationStrategy orchestration strategy} to use * when {@link se.arkalix.ArSystem#consume() resolving what services} * to consume. * * @param orchestrationStrategy Desired orchestration strategy. * @return This builder. */ public Builder orchestrationStrategy(final ArOrchestrationStrategy orchestrationStrategy) { this.orchestrationStrategy = orchestrationStrategy; return this; } /** * @return New {@link HttpJsonCloudPlugin}. */ public HttpJsonCloudPlugin build() { return new HttpJsonCloudPlugin(this); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy