Преамбула

В одном из самодельных процессоров Apache NiFi у меня возникла необходимость работать с файлами сертификатов. Так как в проекте уже были настроенные службы сертификатов (типа StandardRestrictedSSLContextService), я решил сделать у процессора свойство с типом SSLContextService, чтобы можно было подставлять уже настроенные в NiFi службы контроллера и брать данные сертификатов оттуда. Изучил матчасть, ничего сложного. Добавил свойство в процессор, закинул его на flow. Но... процессор не видит мои StandardRestrictedSSLContextService. Пересмотрел и перечитал множество статей, в том числе от уважаемого Pierre Villard, но никак. Пока не наткнулся на реализацию похожего кейса на Github.

Возможно, это банальная проблема, но у меня она вызвала приличные затруднения, поэтому я решил об этом написать. Ниже приведу примеры кода, а так же покажу, как подсунуть свою заглушку в интеграционный тест процессора (что тоже оказалось не совсем очевидной задачей).

Добавление свойства в процессор

Добавляем в процессор свойство, через которое можно будет указывать стандартные службы, обеспечивающие доступ к сертификатам.

public static final PropertyDescriptor SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder()
    .name("SSL Context Service")
    .description("SSL Context Service provides trusted certificates and client certificates for TLS communication.")
    .required(false)
    .identifiesControllerService(SSLContextService.class)
    .build();

В коде метода @OnTrigger процессора доступ к значению свойства будем получать вот так:

SSLContextService sslService = context.getProperty(SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class);

Теперь наверно самый важный участок этой маленькой статьи, ради которого все и затевалось. Правильное добавление зависимостей!

1) В pom.xml основного модуля процессора добавим две зависимости:

<dependency>
    <groupId>org.apache.nifi</groupId>
    <artifactId>nifi-ssl-context-service-api</artifactId>
    <version>${nifi.version}</version>
</dependency>
 
<dependency>
    <groupId>org.apache.nifi</groupId>
    <artifactId>nifi-standard-services-api-nar</artifactId>
    <version>${nifi.version}</version>
    <type>nar</type>
</dependency>

2) В pom.xml nar-модуля добавим одну зависимость:

<dependency>
    <groupId>org.apache.nifi</groupId>
    <artifactId>nifi-standard-services-api-nar</artifactId>
    <version>${nifi.version}</version>
    <type>nar</type>
</dependency>

nifi-ssl-context-service-api - с помощью этой зависимости в модуле процессора у нас появляется возможность использовать интерфейс SSLContextService.class в коде основного модуля процессора

nifi-standard-services-api-nar - эта зависимость в обоих модулях позволит процессору использовать стандартные службы NiFi

Собственно, вот и весь фокус.

Если надо понять, какую зависимость придется использовать, когда нужно добавить в процессор свойство, отличное по типу от SSLContextService, то помочь в этом может анализ исходников NiFi.

Тестирование процессора, в котором используется служба в качестве одного из свойств

Небольшая проблема заключается в том, что для тестирования процессора, у которого одно из свойств это Controller Service, нам надо обязательно создать некую заглушку этого сервиса. А потом и указать ее в качестве одного из свойств.

Подготовим вспомогательные тестовые данные:

DummySSLContextService.class - заглушка для службы
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import lombok.Builder;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.controller.AbstractControllerService;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.security.util.TlsConfiguration;
import org.apache.nifi.ssl.SSLContextService;

@Builder
public class DummySSLContextService extends AbstractControllerService implements SSLContextService {
    private final String keyStoreFile;
    private final String keyStorePassword;
    private final String keyStoreType;
    private final String trustStoreFile;
    private final String trustStorePassword;
    private final String trustStoreType;

    @Override
    public TlsConfiguration createTlsConfiguration() {
        throw new RuntimeException("Method not implemented");
    }

    @Override
    public SSLContext createContext() {
        throw new RuntimeException("Method not implemented");
    }

    @Override
    public SSLContext createSSLContext(org.apache.nifi.security.util.ClientAuth clientAuth) throws ProcessException {
        throw new RuntimeException("Method not implemented");
    }

    @Override
    public SSLContext createSSLContext(ClientAuth clientAuth) throws ProcessException {
        throw new RuntimeException("Method not implemented");
    }

    @Override
    public X509TrustManager createTrustManager() {
        throw new RuntimeException("Method not implemented");
    }

    @Override
    public String getTrustStoreFile() {
        return trustStoreFile;
    }

    @Override
    public String getTrustStoreType() {
        return trustStoreType;
    }

    @Override
    public String getTrustStorePassword() {
        return trustStorePassword;
    }

    @Override
    public boolean isTrustStoreConfigured() {
        throw new RuntimeException("Method not implemented");
    }

    @Override
    public String getKeyStoreFile() {
        return keyStoreFile;
    }

    @Override
    public String getKeyStoreType() {
        return keyStoreType;
    }

    @Override
    public String getKeyStorePassword() {
        return keyStorePassword;
    }

    @Override
    public String getKeyPassword() {
        throw new RuntimeException("Method not implemented");
    }

    @Override
    public boolean isKeyStoreConfigured() {
        throw new RuntimeException("Method not implemented");
    }

    @Override
    public String getSslAlgorithm() {
        throw new RuntimeException("Method not implemented");
    }

    @Override
    public void onPropertyModified(PropertyDescriptor propertyDescriptor, String s, String s1) {
        // ничего не делаем
    }
}

TestData.class - константы и экземпляр службы-заглушки для тестов
import java.nio.file.Path;

import org.apache.nifi.ssl.SSLContextService;

public class TestData {
    static final String CLIENT_CERT_FILE_NAME = Path.of("src/test/resources/client-identity.jks").toAbsolutePath().toString();
    static final String CLIENT_TRUST_FILE_NAME = Path.of("src/test/resources/truststore.jks").toAbsolutePath().toString();
    public static final String KEYSTORE_COMMON_PASSWORD = "changeit";
    public static final String KEYSTORE_COMMON_TYPE = "JKS";

    public static final SSLContextService TEST_SSL_CONTEXT_SERVICE = DummySSLContextService.builder()
            .keyStoreFile(CLIENT_CERT_FILE_NAME)
            .keyStorePassword(KEYSTORE_COMMON_PASSWORD)
            .keyStoreType(KEYSTORE_COMMON_TYPE)
            .trustStoreFile(CLIENT_TRUST_FILE_NAME)
            .trustStorePassword(KEYSTORE_COMMON_PASSWORD)
            .trustStoreType(KEYSTORE_COMMON_TYPE)
            .build();
}

И собственно тест (тут SSL_CONTEXT_SERVICE это дескриптор свойства процессора):

    @Test
    void should_run_with_service_property() throws InitializationException {
        final TestRunner testRunner = TestRunners.newTestRunner(MyProcessor.class);

        // устанавливаем значение для одного из свойств тестового процессора
        testRunner.setProperty(SOME_PROPERTY_NAME, SOME_PROPERTY_VALUE);
      
        // устанавливаем для тестового процессора свойство, в котором указывается Controller Service
        // это делается в три шага
        testRunner.addControllerService("TestSSLContextService", TEST_SSL_CONTEXT_SERVICE);
        testRunner.setProperty(SSL_CONTEXT_SERVICE, "TestSSLContextService");
        testRunner.enableControllerService(TEST_SSL_CONTEXT_SERVICE);

        // создаем набор аттрибутов для тестового flow-файла
        Map<String, String> attributes = new HashMap<>();
        attributes.put(SOME_ATTR_NAME, SOME_ATTR_VALUE);

        // добавляем контент и аттрибуты в тестовый flow-файл
        // вместо строки сюда может быть передан поток
        testRunner.enqueue("Flowfile content", attributes);

        // When
        testRunner.run();

        // Then
        List<MockFlowFile> originalFlowFiles = testRunner.getFlowFilesForRelationship(REL_SUCCESS);
        List<MockFlowFile> failureFlowFiles = testRunner.getFlowFilesForRelationship(REL_FAILURE);

        assertThat(originalFlowFiles.size()).isEqualTo(1);
        assertThat(failureFlowFiles.size()).isZero();

        Map<String, String> actualAttributes = originalFlowFiles.get(0).getAttributes();
        assertThat(actualAttributes)
                .isNotNull()
                .containsEntry(SOME_ATTR_NAME, SOME_ATTR_VALUE);
    }

Заключение

Вот и все. Надеюсь эта маленькая статья будет полезна. Замечания и указания на неточности в комментариях очень приветствуются))

Спасибо, что дочитали до конца.

Комментарии (0)