This is an automated email from the ASF dual-hosted git repository.

fgerlits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git

commit 90ddaae1476a96b5cc85d6ea54f3cd1c2e007b3d
Author: Gabor Gyimesi <[email protected]>
AuthorDate: Fri Nov 14 11:46:12 2025 +0100

    MINIFICPP-2678 Move Grafana Loki tests to modular docker tests
    
    Closes #2072
    
    Signed-off-by: Ferenc Gerlits <[email protected]>
---
 .../containers/docker_image_builder.py             |   9 +-
 docker/RunBehaveTests.sh                           |   3 +-
 docker/test/integration/cluster/ContainerStore.py  |  27 ----
 .../test/integration/cluster/DockerTestCluster.py  |  12 --
 docker/test/integration/cluster/ImageStore.py      |   5 -
 .../cluster/checkers/GrafanaLokiChecker.py         |  54 -------
 .../cluster/containers/GrafanaLokiContainer.py     | 176 ---------------------
 .../cluster/containers/ReverseProxyContainer.py    |  43 -----
 .../features/MiNiFi_integration_test_driver.py     |  10 --
 docker/test/integration/features/steps/steps.py    |  43 -----
 .../minifi/processors/PushGrafanaLokiGrpc.py       |  24 ---
 .../minifi/processors/PushGrafanaLokiREST.py       |  24 ---
 .../integration/resources/reverse-proxy/Dockerfile |  17 --
 .../resources/reverse-proxy/nginx-basic-auth.conf  |  12 --
 .../integration/resources/reverse-proxy/run.sh     |   5 -
 .../grafana-loki/tests/features/environment.py     |  49 ++++++
 .../tests}/features/grafana_loki.feature           |  44 ++++--
 .../resources/check_log_lines_on_grafana.py        |  79 +++++++++
 .../features/resources/reverse-proxy/Dockerfile    |   8 +
 .../resources/reverse-proxy/nginx.conf.template    |  10 ++
 .../tests/features/resources/reverse-proxy/run.sh  |   8 +
 .../tests/features/steps/grafana_loki_container.py | 151 ++++++++++++++++++
 .../features/steps/reverse_proxy_container.py      |  38 +++++
 .../grafana-loki/tests/features/steps/steps.py     |  63 ++++++++
 24 files changed, 448 insertions(+), 466 deletions(-)

diff --git 
a/behave_framework/src/minifi_test_framework/containers/docker_image_builder.py 
b/behave_framework/src/minifi_test_framework/containers/docker_image_builder.py
index 658ccedb9..1e5d5e602 100644
--- 
a/behave_framework/src/minifi_test_framework/containers/docker_image_builder.py
+++ 
b/behave_framework/src/minifi_test_framework/containers/docker_image_builder.py
@@ -24,7 +24,7 @@ from docker.models.images import Image
 
 
 class DockerImageBuilder:
-    def __init__(self, image_tag: str, dockerfile_content: str | None = None, 
build_context_path: str | None = None):
+    def __init__(self, image_tag: str, dockerfile_content: str | None = None, 
files_on_context: dict[str, bytes] | None = None, build_context_path: str | 
None = None):
         if not dockerfile_content and not build_context_path:
             raise ValueError("Either 'dockerfile_content' or 
'build_context_path' must be provided.")
         if dockerfile_content and build_context_path:
@@ -32,6 +32,7 @@ class DockerImageBuilder:
 
         self.image_tag: str = image_tag
         self.dockerfile_content: str | None = dockerfile_content
+        self.files_on_context: dict[str, str] | None = files_on_context
         self.build_context_path: str | None = build_context_path
         self.client = docker.from_env()
         self.image: Image | None = None
@@ -46,6 +47,12 @@ class DockerImageBuilder:
             with open(dockerfile_path, 'w') as f:
                 f.write(self.dockerfile_content)
 
+            if self.files_on_context:
+                for filename, content in self.files_on_context.items():
+                    file_path = os.path.join(context_path, filename)
+                    with open(file_path, 'wb') as f:
+                        f.write(content)
+
         logging.info(f"Building Docker image '{self.image_tag}' from context 
'{context_path}'...")
         try:
             self.image, build_logs = 
self.client.images.build(path=context_path, tag=self.image_tag, rm=True, 
forcerm=True)
diff --git a/docker/RunBehaveTests.sh b/docker/RunBehaveTests.sh
index 4a6e9b182..65b5d43d5 100755
--- a/docker/RunBehaveTests.sh
+++ b/docker/RunBehaveTests.sh
@@ -204,4 +204,5 @@ exec \
     "${docker_dir}/../extensions/couchbase/tests/features" \
     "${docker_dir}/../extensions/elasticsearch/tests/features" \
     "${docker_dir}/../extensions/splunk/tests/features" \
-    "${docker_dir}/../extensions/gcp/tests/features"
+    "${docker_dir}/../extensions/gcp/tests/features" \
+    "${docker_dir}/../extensions/grafana-loki/tests/features"
diff --git a/docker/test/integration/cluster/ContainerStore.py 
b/docker/test/integration/cluster/ContainerStore.py
index d7cb787c3..10b093dee 100644
--- a/docker/test/integration/cluster/ContainerStore.py
+++ b/docker/test/integration/cluster/ContainerStore.py
@@ -29,9 +29,6 @@ from .containers.SyslogTcpClientContainer import 
SyslogTcpClientContainer
 from .containers.MinifiAsPodInKubernetesCluster import 
MinifiAsPodInKubernetesCluster
 from .containers.PrometheusContainer import PrometheusContainer
 from .containers.MinifiC2ServerContainer import MinifiC2ServerContainer
-from .containers.GrafanaLokiContainer import GrafanaLokiContainer
-from .containers.GrafanaLokiContainer import GrafanaLokiOptions
-from .containers.ReverseProxyContainer import ReverseProxyContainer
 from .FeatureContext import FeatureContext
 
 
@@ -39,7 +36,6 @@ class ContainerStore:
     def __init__(self, network, image_store, kubernetes_proxy, feature_id):
         self.feature_id = feature_id
         self.minifi_options = MinifiOptions()
-        self.grafana_loki_options = GrafanaLokiOptions()
         self.containers = {}
         self.data_directories = {}
         self.network = network
@@ -212,23 +208,6 @@ class ContainerStore:
                                                                       
image_store=self.image_store,
                                                                       
command=command,
                                                                       
ssl=True))
-        elif engine == "grafana-loki-server":
-            return self.containers.setdefault(container_name,
-                                              
GrafanaLokiContainer(feature_context=feature_context,
-                                                                   
name=container_name,
-                                                                   
vols=self.vols,
-                                                                   
network=self.network,
-                                                                   
image_store=self.image_store,
-                                                                   
options=self.grafana_loki_options,
-                                                                   
command=command))
-        elif engine == "reverse-proxy":
-            return self.containers.setdefault(container_name,
-                                              
ReverseProxyContainer(feature_context=feature_context,
-                                                                    
name=container_name,
-                                                                    
vols=self.vols,
-                                                                    
network=self.network,
-                                                                    
image_store=self.image_store,
-                                                                    
command=command))
         else:
             raise Exception('invalid flow engine: \'%s\'' % engine)
 
@@ -345,12 +324,6 @@ class ContainerStore:
     def get_container_names(self, engine=None):
         return [key for key in self.containers.keys() if not engine or 
self.containers[key].get_engine() == engine]
 
-    def enable_ssl_in_grafana_loki(self):
-        self.grafana_loki_options.enable_ssl = True
-
-    def enable_multi_tenancy_in_grafana_loki(self):
-        self.grafana_loki_options.enable_multi_tenancy = True
-
     def enable_ssl_in_nifi(self):
         self.nifi_options.use_ssl = True
 
diff --git a/docker/test/integration/cluster/DockerTestCluster.py 
b/docker/test/integration/cluster/DockerTestCluster.py
index 874708420..15714f5ea 100644
--- a/docker/test/integration/cluster/DockerTestCluster.py
+++ b/docker/test/integration/cluster/DockerTestCluster.py
@@ -20,7 +20,6 @@ import tempfile
 import os
 import gzip
 import shutil
-from typing import List
 
 from .LogSource import LogSource
 from .ContainerStore import ContainerStore
@@ -30,7 +29,6 @@ from .checkers.AwsChecker import AwsChecker
 from .checkers.AzureChecker import AzureChecker
 from .checkers.PostgresChecker import PostgresChecker
 from .checkers.PrometheusChecker import PrometheusChecker
-from .checkers.GrafanaLokiChecker import GrafanaLokiChecker
 from .checkers.ModbusChecker import ModbusChecker
 from .checkers.MqttHelper import MqttHelper
 from utils import get_peak_memory_usage, get_minifi_pid, get_memory_usage, 
retry_check
@@ -46,7 +44,6 @@ class DockerTestCluster:
         self.azure_checker = AzureChecker(self.container_communicator)
         self.postgres_checker = PostgresChecker(self.container_communicator)
         self.prometheus_checker = PrometheusChecker()
-        self.grafana_loki_checker = GrafanaLokiChecker()
         self.minifi_controller_executor = 
MinifiControllerExecutor(self.container_communicator)
         self.modbus_checker = ModbusChecker(self.container_communicator)
         self.mqtt_helper = MqttHelper()
@@ -108,12 +105,6 @@ class DockerTestCluster:
     def enable_sql_in_minifi(self):
         self.container_store.enable_sql_in_minifi()
 
-    def enable_ssl_in_grafana_loki(self):
-        self.container_store.enable_ssl_in_grafana_loki()
-
-    def enable_multi_tenancy_in_grafana_loki(self):
-        self.container_store.enable_multi_tenancy_in_grafana_loki()
-
     def 
use_nifi_python_processors_with_system_python_packages_installed_in_minifi(self):
         
self.container_store.use_nifi_python_processors_with_system_python_packages_installed_in_minifi()
 
@@ -386,9 +377,6 @@ class DockerTestCluster:
 
             return True
 
-    def wait_for_lines_on_grafana_loki(self, lines: List[str], 
timeout_seconds: int, ssl: bool, tenant_id: str):
-        return self.grafana_loki_checker.wait_for_lines_on_grafana_loki(lines, 
timeout_seconds, ssl, tenant_id)
-
     def set_value_on_plc_with_modbus(self, container_name, modbus_cmd):
         return 
self.modbus_checker.set_value_on_plc_with_modbus(container_name, modbus_cmd)
 
diff --git a/docker/test/integration/cluster/ImageStore.py 
b/docker/test/integration/cluster/ImageStore.py
index 4e52f7deb..360af8a7c 100644
--- a/docker/test/integration/cluster/ImageStore.py
+++ b/docker/test/integration/cluster/ImageStore.py
@@ -67,8 +67,6 @@ class ImageStore:
             image = self.__build_mqtt_broker_image()
         elif container_engine == "kinesis-server":
             image = self.__build_kinesis_image()
-        elif container_engine == "reverse-proxy":
-            image = self.__build_reverse_proxy_image()
         else:
             raise Exception("There is no associated image for " + 
container_engine)
 
@@ -293,9 +291,6 @@ class ImageStore:
     def __build_kinesis_image(self):
         return self.__build_image_by_path(self.test_dir + 
"/resources/kinesis-mock", 'kinesis-server')
 
-    def __build_reverse_proxy_image(self):
-        return self.__build_image_by_path(self.test_dir + 
"/resources/reverse-proxy", 'reverse-proxy')
-
     def __build_image(self, dockerfile, context_files=[]):
         conf_dockerfile_buffer = BytesIO()
         docker_context_buffer = BytesIO()
diff --git a/docker/test/integration/cluster/checkers/GrafanaLokiChecker.py 
b/docker/test/integration/cluster/checkers/GrafanaLokiChecker.py
deleted file mode 100644
index da78f2a20..000000000
--- a/docker/test/integration/cluster/checkers/GrafanaLokiChecker.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You 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.
-import requests
-from typing import List
-from utils import wait_for
-
-
-class GrafanaLokiChecker:
-    def __init__(self):
-        self.url = "localhost:3100/loki/api/v1/query_range"
-
-    def veify_log_lines_on_grafana_loki(self, lines: List[str], ssl: bool, 
tenant_id: str):
-        labels = '{job="minifi"}'
-        prefix = "http://";
-        if ssl:
-            prefix = "https://";
-
-        query_url = f"{prefix}{self.url}?query={labels}"
-
-        headers = None
-        if tenant_id:
-            headers = {'X-Scope-OrgID': tenant_id}
-
-        response = requests.get(query_url, verify=False, timeout=30, 
headers=headers)
-        if response.status_code < 200 or response.status_code >= 300:
-            return False
-
-        json_response = response.json()
-        if "data" not in json_response or "result" not in 
json_response["data"] or len(json_response["data"]["result"]) < 1:
-            return False
-
-        result = json_response["data"]["result"][0]
-        if "values" not in result:
-            return False
-
-        for line in lines:
-            if line not in str(result["values"]):
-                return False
-        return True
-
-    def wait_for_lines_on_grafana_loki(self, lines: List[str], 
timeout_seconds: int, ssl: bool, tenant_id: str):
-        return wait_for(lambda: self.veify_log_lines_on_grafana_loki(lines, 
ssl, tenant_id), timeout_seconds)
diff --git a/docker/test/integration/cluster/containers/GrafanaLokiContainer.py 
b/docker/test/integration/cluster/containers/GrafanaLokiContainer.py
deleted file mode 100644
index a46cd2dbf..000000000
--- a/docker/test/integration/cluster/containers/GrafanaLokiContainer.py
+++ /dev/null
@@ -1,176 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You 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.
-import logging
-import os
-import tempfile
-import docker.types
-import OpenSSL.crypto
-
-from .Container import Container
-from ssl_utils.SSL_cert_utils import make_server_cert
-
-
-class GrafanaLokiOptions:
-    def __init__(self):
-        self.enable_ssl = False
-        self.enable_multi_tenancy = False
-
-
-class GrafanaLokiContainer(Container):
-    def __init__(self, feature_context, name, vols, network, image_store, 
options: GrafanaLokiOptions, command=None):
-        super().__init__(feature_context, name, "grafana-loki-server", vols, 
network, image_store, command)
-        self.ssl = options.enable_ssl
-        extra_ssl_settings = ""
-        if self.ssl:
-            grafana_loki_cert, grafana_loki_key = 
make_server_cert(f"grafana-loki-server-{feature_context.id}", 
feature_context.root_ca_cert, feature_context.root_ca_key)
-
-            self.root_ca_file = tempfile.NamedTemporaryFile(delete=False)
-            
self.root_ca_file.write(OpenSSL.crypto.dump_certificate(type=OpenSSL.crypto.FILETYPE_PEM,
 cert=feature_context.root_ca_cert))
-            self.root_ca_file.close()
-            os.chmod(self.root_ca_file.name, 0o644)
-
-            self.grafana_loki_cert_file = 
tempfile.NamedTemporaryFile(delete=False)
-            
self.grafana_loki_cert_file.write(OpenSSL.crypto.dump_certificate(type=OpenSSL.crypto.FILETYPE_PEM,
 cert=grafana_loki_cert))
-            self.grafana_loki_cert_file.close()
-            os.chmod(self.grafana_loki_cert_file.name, 0o644)
-
-            self.grafana_loki_key_file = 
tempfile.NamedTemporaryFile(delete=False)
-            
self.grafana_loki_key_file.write(OpenSSL.crypto.dump_privatekey(type=OpenSSL.crypto.FILETYPE_PEM,
 pkey=grafana_loki_key))
-            self.grafana_loki_key_file.close()
-            os.chmod(self.grafana_loki_key_file.name, 0o644)
-
-            extra_ssl_settings = """
-  http_tls_config:
-    cert_file: /etc/loki/cert.pem
-    key_file: /etc/loki/key.pem
-    client_ca_file: /etc/loki/root_ca.crt
-    client_auth_type: VerifyClientCertIfGiven
-
-  grpc_tls_config:
-    cert_file: /etc/loki/cert.pem
-    key_file: /etc/loki/key.pem
-    client_ca_file: /etc/loki/root_ca.crt
-    client_auth_type: VerifyClientCertIfGiven
-
-query_scheduler:
-  grpc_client_config:
-    grpc_compression: snappy
-    tls_enabled: true
-    tls_ca_path: /etc/loki/root_ca.crt
-    tls_insecure_skip_verify: true
-
-ingester_client:
-  grpc_client_config:
-    grpc_compression: snappy
-    tls_enabled: true
-    tls_ca_path: /etc/loki/root_ca.crt
-    tls_insecure_skip_verify: true
-
-frontend:
-  grpc_client_config:
-    grpc_compression: snappy
-    tls_enabled: true
-    tls_ca_path: /etc/loki/root_ca.crt
-    tls_insecure_skip_verify: true
-
-frontend_worker:
-  grpc_client_config:
-    grpc_compression: snappy
-    tls_enabled: true
-    tls_ca_path: /etc/loki/root_ca.crt
-    tls_insecure_skip_verify: true
-"""
-
-        grafana_loki_yml_content = """
-auth_enabled: {enable_multi_tenancy}
-
-server:
-  http_listen_port: 3100
-  grpc_listen_port: 9095
-{extra_ssl_settings}
-
-common:
-  path_prefix: /loki
-  storage:
-    filesystem:
-      chunks_directory: /loki/chunks
-      rules_directory: /loki/rules
-  replication_factor: 1
-  ring:
-    kvstore:
-      store: inmemory
-
-schema_config:
-  configs:
-    - from: 2020-05-15
-      store: tsdb
-      object_store: filesystem
-      schema: v13
-      index:
-        prefix: index_
-        period: 24h
-
-ruler:
-  alertmanager_url: http://localhost:9093
-
-analytics:
-  reporting_enabled: false
-""".format(extra_ssl_settings=extra_ssl_settings, 
enable_multi_tenancy=options.enable_multi_tenancy)
-
-        self.yaml_file = tempfile.NamedTemporaryFile(delete=False)
-        self.yaml_file.write(grafana_loki_yml_content.encode())
-        self.yaml_file.close()
-        os.chmod(self.yaml_file.name, 0o644)
-
-    def get_startup_finished_log_entry(self):
-        return "Loki started"
-
-    def deploy(self):
-        if not self.set_deployed():
-            return
-
-        logging.info('Creating and running Grafana Loki docker container...')
-
-        mounts = [docker.types.Mount(
-            type='bind',
-            source=self.yaml_file.name,
-            target='/etc/loki/local-config.yaml'
-        )]
-
-        if self.ssl:
-            mounts.append(docker.types.Mount(
-                type='bind',
-                source=self.root_ca_file.name,
-                target='/etc/loki/root_ca.crt'
-            ))
-            mounts.append(docker.types.Mount(
-                type='bind',
-                source=self.grafana_loki_cert_file.name,
-                target='/etc/loki/cert.pem'
-            ))
-            mounts.append(docker.types.Mount(
-                type='bind',
-                source=self.grafana_loki_key_file.name,
-                target='/etc/loki/key.pem'
-            ))
-
-        self.client.containers.run(
-            image="grafana/loki:3.2.1",
-            detach=True,
-            name=self.name,
-            network=self.network.name,
-            ports={'3100/tcp': 3100},
-            mounts=mounts,
-            entrypoint=self.command)
diff --git 
a/docker/test/integration/cluster/containers/ReverseProxyContainer.py 
b/docker/test/integration/cluster/containers/ReverseProxyContainer.py
deleted file mode 100644
index 3cd486343..000000000
--- a/docker/test/integration/cluster/containers/ReverseProxyContainer.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You 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.
-import logging
-from .Container import Container
-
-
-class ReverseProxyContainer(Container):
-    def __init__(self, feature_context, name, vols, network, image_store, 
command=None):
-        super().__init__(feature_context, name, 'reverse-proxy', vols, 
network, image_store, command)
-
-    def get_startup_finished_log_entry(self):
-        return "start worker process"
-
-    def deploy(self):
-        if not self.set_deployed():
-            return
-
-        logging.info('Creating and running reverse-proxy docker container...')
-        self.client.containers.run(
-            self.image_store.get_image(self.get_engine()),
-            detach=True,
-            name=self.name,
-            network=self.network.name,
-            environment=[
-                "BASIC_USERNAME=admin",
-                "BASIC_PASSWORD=password",
-                f"FORWARD_HOST=grafana-loki-server-{self.feature_context.id}",
-                "FORWARD_PORT=3100",
-            ],
-            entrypoint=self.command)
-        logging.info('Added container \'%s\'', self.name)
diff --git a/docker/test/integration/features/MiNiFi_integration_test_driver.py 
b/docker/test/integration/features/MiNiFi_integration_test_driver.py
index 9373c9b99..30c3d7f72 100644
--- a/docker/test/integration/features/MiNiFi_integration_test_driver.py
+++ b/docker/test/integration/features/MiNiFi_integration_test_driver.py
@@ -17,7 +17,6 @@ import os
 import time
 import uuid
 
-from typing import List
 from pydoc import locate
 from minifi.core.InputPort import InputPort
 from minifi.core.OutputPort import OutputPort
@@ -369,12 +368,6 @@ class MiNiFi_integration_test:
     def enable_sql_in_minifi(self):
         self.cluster.enable_sql_in_minifi()
 
-    def enable_ssl_in_grafana_loki(self):
-        self.cluster.enable_ssl_in_grafana_loki()
-
-    def enable_multi_tenancy_in_grafana_loki(self):
-        self.cluster.enable_multi_tenancy_in_grafana_loki()
-
     def 
use_nifi_python_processors_with_system_python_packages_installed_in_minifi(self):
         
self.cluster.use_nifi_python_processors_with_system_python_packages_installed_in_minifi()
 
@@ -447,9 +440,6 @@ class MiNiFi_integration_test:
     def debug_bundle_can_be_retrieved_through_minifi_controller(self, 
container_name: str):
         assert 
self.cluster.debug_bundle_can_be_retrieved_through_minifi_controller(container_name)
 or self.cluster.log_app_output()
 
-    def check_lines_on_grafana_loki(self, lines: List[str], timeout_seconds: 
int, ssl: bool, tenant_id=None):
-        assert self.cluster.wait_for_lines_on_grafana_loki(lines, 
timeout_seconds, ssl, tenant_id) or self.cluster.log_app_output()
-
     def set_value_on_plc_with_modbus(self, container_name, modbus_cmd):
         assert self.cluster.set_value_on_plc_with_modbus(container_name, 
modbus_cmd)
 
diff --git a/docker/test/integration/features/steps/steps.py 
b/docker/test/integration/features/steps/steps.py
index f317294f2..1150a569a 100644
--- a/docker/test/integration/features/steps/steps.py
+++ b/docker/test/integration/features/steps/steps.py
@@ -1052,43 +1052,6 @@ def step_impl(context):
     context.execute_steps(f"then debug bundle can be retrieved through MiNiFi 
controller in the \"minifi-cpp-flow-{context.feature_id}\" flow")
 
 
-# Grafana Loki
-@given("a Grafana Loki server is set up")
-def step_impl(context):
-    context.test.acquire_container(context=context, 
name="grafana-loki-server", engine="grafana-loki-server")
-
-
-@given("a Grafana Loki server with SSL is set up")
-def step_impl(context):
-    context.test.enable_ssl_in_grafana_loki()
-    context.test.acquire_container(context=context, 
name="grafana-loki-server", engine="grafana-loki-server")
-
-
-@given("a Grafana Loki server is set up with multi-tenancy enabled")
-def step_impl(context):
-    context.test.enable_multi_tenancy_in_grafana_loki()
-    context.test.acquire_container(context=context, 
name="grafana-loki-server", engine="grafana-loki-server")
-
-
-@then("\"{lines}\" lines are published to the Grafana Loki server in less than 
{timeout_seconds:d} seconds")
-@then("\"{lines}\" line is published to the Grafana Loki server in less than 
{timeout_seconds:d} seconds")
-def step_impl(context, lines: str, timeout_seconds: int):
-    context.test.check_lines_on_grafana_loki(lines.split(";"), 
timeout_seconds, False)
-
-
-@then("\"{lines}\" lines are published to the \"{tenant_id}\" tenant on the 
Grafana Loki server in less than {timeout_seconds:d} seconds")
-@then("\"{lines}\" line is published to the \"{tenant_id}\" tenant on the 
Grafana Loki server in less than {timeout_seconds:d} seconds")
-def step_impl(context, lines: str, tenant_id: str, timeout_seconds: int):
-    context.test.check_lines_on_grafana_loki(lines.split(";"), 
timeout_seconds, False, tenant_id)
-
-
-@then("\"{lines}\" lines are published using SSL to the Grafana Loki server in 
less than {timeout_seconds:d} seconds")
-@then("\"{lines}\" line is published using SSL to the Grafana Loki server in 
less than {timeout_seconds:d} seconds")
-def step_impl(context, lines: str, timeout_seconds: int):
-    context.test.check_lines_on_grafana_loki(lines.split(";"), 
timeout_seconds, True)
-
-
-@given(u'a SSL context service is set up for Grafana Loki processor 
\"{processor_name}\"')
 @given(u'a SSL context service is set up for the following processor: 
\"{processor_name}\"')
 def step_impl(context, processor_name: str):
     setUpSslContextServiceForProcessor(context, processor_name)
@@ -1099,12 +1062,6 @@ def step_impl(context, remote_process_group: str):
     setUpSslContextServiceForRPG(context, remote_process_group)
 
 
-# Nginx reverse proxy
-@given(u'a reverse proxy is set up to forward requests to the Grafana Loki 
server')
-def step_impl(context):
-    context.test.acquire_container(context=context, name="reverse-proxy", 
engine="reverse-proxy")
-
-
 # Python
 @given("python with langchain is installed on the MiNiFi agent {install_mode}")
 def step_impl(context, install_mode):
diff --git a/docker/test/integration/minifi/processors/PushGrafanaLokiGrpc.py 
b/docker/test/integration/minifi/processors/PushGrafanaLokiGrpc.py
deleted file mode 100644
index f938d3ef1..000000000
--- a/docker/test/integration/minifi/processors/PushGrafanaLokiGrpc.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You 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.
-from ..core.Processor import Processor
-
-
-class PushGrafanaLokiGrpc(Processor):
-    def __init__(self, context, schedule={'scheduling strategy': 
'EVENT_DRIVEN'}):
-        super(PushGrafanaLokiGrpc, self).__init__(
-            context=context,
-            clazz='PushGrafanaLokiGrpc',
-            auto_terminate=['success', 'failure'],
-            schedule=schedule)
diff --git a/docker/test/integration/minifi/processors/PushGrafanaLokiREST.py 
b/docker/test/integration/minifi/processors/PushGrafanaLokiREST.py
deleted file mode 100644
index df4beab28..000000000
--- a/docker/test/integration/minifi/processors/PushGrafanaLokiREST.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You 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.
-from ..core.Processor import Processor
-
-
-class PushGrafanaLokiREST(Processor):
-    def __init__(self, context, schedule={'scheduling strategy': 
'EVENT_DRIVEN'}):
-        super(PushGrafanaLokiREST, self).__init__(
-            context=context,
-            clazz='PushGrafanaLokiREST',
-            auto_terminate=['success', 'failure'],
-            schedule=schedule)
diff --git a/docker/test/integration/resources/reverse-proxy/Dockerfile 
b/docker/test/integration/resources/reverse-proxy/Dockerfile
deleted file mode 100644
index a479f117f..000000000
--- a/docker/test/integration/resources/reverse-proxy/Dockerfile
+++ /dev/null
@@ -1,17 +0,0 @@
-# Source: https://gist.github.com/laurentbel/c4c7696890fc71c8061172a932eb52e4
-FROM nginx:1.25.3
-
-RUN apt-get update -y && apt-get install -y apache2-utils && rm -rf 
/var/lib/apt/lists/*
-
-ENV BASIC_USERNAME=username
-ENV BASIC_PASSWORD=password
-
-ENV FORWARD_HOST=google.com
-ENV FORWARD_PORT=80
-
-WORKDIR /
-COPY nginx-basic-auth.conf nginx-basic-auth.conf
-
-COPY run.sh ./
-RUN chmod 0755 ./run.sh
-CMD [ "./run.sh" ]
diff --git 
a/docker/test/integration/resources/reverse-proxy/nginx-basic-auth.conf 
b/docker/test/integration/resources/reverse-proxy/nginx-basic-auth.conf
deleted file mode 100644
index c64c587c3..000000000
--- a/docker/test/integration/resources/reverse-proxy/nginx-basic-auth.conf
+++ /dev/null
@@ -1,12 +0,0 @@
-# Source: https://gist.github.com/laurentbel/c4c7696890fc71c8061172a932eb52e4
-server {
- listen 3030 default_server;
-
- location / {
-     auth_basic             "Restricted";
-     auth_basic_user_file   .htpasswd;
-
-     proxy_pass             http://${FORWARD_HOST}:${FORWARD_PORT};
-     proxy_read_timeout     900;
- }
-}
diff --git a/docker/test/integration/resources/reverse-proxy/run.sh 
b/docker/test/integration/resources/reverse-proxy/run.sh
deleted file mode 100644
index c9c6c436c..000000000
--- a/docker/test/integration/resources/reverse-proxy/run.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/sh
-# Source: https://gist.github.com/laurentbel/c4c7696890fc71c8061172a932eb52e4
-envsubst < nginx-basic-auth.conf > /etc/nginx/conf.d/default.conf
-htpasswd -c -b /etc/nginx/.htpasswd "${BASIC_USERNAME}" "${BASIC_PASSWORD}"
-exec nginx -g "daemon off;"
diff --git a/extensions/grafana-loki/tests/features/environment.py 
b/extensions/grafana-loki/tests/features/environment.py
new file mode 100644
index 000000000..11ab6ba74
--- /dev/null
+++ b/extensions/grafana-loki/tests/features/environment.py
@@ -0,0 +1,49 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+from pathlib import Path
+from minifi_test_framework.containers.docker_image_builder import 
DockerImageBuilder
+from minifi_test_framework.core.hooks import common_before_scenario
+from minifi_test_framework.core.hooks import common_after_scenario
+
+
+def before_all(context):
+    check_log_lines_path = Path(__file__).resolve().parent / "resources" / 
"check_log_lines_on_grafana.py"
+    check_log_lines_content = None
+    with open(check_log_lines_path, "rb") as f:
+        check_log_lines_content = f.read()
+    dockerfile = """
+FROM python:3.13-slim-bookworm
+RUN pip install requests
+COPY check_log_lines_on_grafana.py /scripts/check_log_lines_on_grafana.py"""
+    grafana_helper_builder = DockerImageBuilder(
+        image_tag="minifi-grafana-loki-helper:latest",
+        dockerfile_content=dockerfile,
+        files_on_context={"check_log_lines_on_grafana.py": 
check_log_lines_content}
+    )
+    grafana_helper_builder.build()
+
+    reverse_proxy_builder = DockerImageBuilder(
+        image_tag="minifi-reverse-proxy:latest",
+        build_context_path=str(Path(__file__).resolve().parent / "resources" / 
"reverse-proxy")
+    )
+    reverse_proxy_builder.build()
+
+
+def before_scenario(context, scenario):
+    common_before_scenario(context, scenario)
+
+
+def after_scenario(context, scenario):
+    common_after_scenario(context, scenario)
diff --git a/docker/test/integration/features/grafana_loki.feature 
b/extensions/grafana-loki/tests/features/grafana_loki.feature
similarity index 82%
rename from docker/test/integration/features/grafana_loki.feature
rename to extensions/grafana-loki/tests/features/grafana_loki.feature
index 478d4c043..d23bbf212 100644
--- a/docker/test/integration/features/grafana_loki.feature
+++ b/extensions/grafana-loki/tests/features/grafana_loki.feature
@@ -16,16 +16,16 @@
 @ENABLE_GRAFANA_LOKI
 Feature: MiNiFi can publish logs to Grafana Loki server
 
-  Background:
-    Given the content of "/tmp/output" is monitored
-
   Scenario: Logs are published to Loki server through REST API
     Given a Grafana Loki server is set up
     And a TailFile processor with the "File to Tail" property set to 
"/tmp/input/test_file.log"
     And a file with filename "test_file.log" and content "log line 1\nlog line 
2\nlog line 3\n" is present in "/tmp/input"
-    And a PushGrafanaLokiREST processor with the "Url" property set to 
"http://grafana-loki-server-${feature_id}:3100/";
+    And a PushGrafanaLokiREST processor with the "Url" property set to 
"http://grafana-loki-server-${scenario_id}:3100/";
+    And PushGrafanaLokiREST is EVENT_DRIVEN
     And the "Stream Labels" property of the PushGrafanaLokiREST processor is 
set to "job=minifi,id=docker-test"
     And the "success" relationship of the TailFile processor is connected to 
the PushGrafanaLokiREST
+    And PushGrafanaLokiREST's success relationship is auto-terminated
+
     When all instances start up
     Then "log line 1;log line 2;log line 3" lines are published to the Grafana 
Loki server in less than 60 seconds
 
@@ -33,10 +33,13 @@ Feature: MiNiFi can publish logs to Grafana Loki server
     Given a Grafana Loki server is set up with multi-tenancy enabled
     And a TailFile processor with the "File to Tail" property set to 
"/tmp/input/test_file.log"
     And a file with filename "test_file.log" and content "log line 1\nlog line 
2\nlog line 3\n" is present in "/tmp/input"
-    And a PushGrafanaLokiREST processor with the "Url" property set to 
"http://grafana-loki-server-${feature_id}:3100/";
+    And a PushGrafanaLokiREST processor with the "Url" property set to 
"http://grafana-loki-server-${scenario_id}:3100/";
+    And PushGrafanaLokiREST is EVENT_DRIVEN
     And the "Stream Labels" property of the PushGrafanaLokiREST processor is 
set to "job=minifi,id=docker-test"
     And the "Tenant ID" property of the PushGrafanaLokiREST processor is set 
to "mytenant"
     And the "success" relationship of the TailFile processor is connected to 
the PushGrafanaLokiREST
+    And PushGrafanaLokiREST's success relationship is auto-terminated
+
     When all instances start up
     Then "log line 1;log line 2;log line 3" lines are published to the 
"mytenant" tenant on the Grafana Loki server in less than 60 seconds
 
@@ -44,10 +47,14 @@ Feature: MiNiFi can publish logs to Grafana Loki server
     Given a Grafana Loki server with SSL is set up
     And a TailFile processor with the "File to Tail" property set to 
"/tmp/input/test_file.log"
     And a file with filename "test_file.log" and content "log line 1\nlog line 
2\nlog line 3\n" is present in "/tmp/input"
-    And a PushGrafanaLokiREST processor with the "Url" property set to 
"https://grafana-loki-server-${feature_id}:3100/";
+    And a PushGrafanaLokiREST processor with the "Url" property set to 
"https://grafana-loki-server-${scenario_id}:3100/";
+    And PushGrafanaLokiREST is EVENT_DRIVEN
     And the "Stream Labels" property of the PushGrafanaLokiREST processor is 
set to "job=minifi,id=docker-test"
     And the "success" relationship of the TailFile processor is connected to 
the PushGrafanaLokiREST
-    And a SSL context service is set up for Grafana Loki processor 
"PushGrafanaLokiREST"
+    And the "SSL Context Service" property of the PushGrafanaLokiREST 
processor is set to "SSLContextService"
+    And an ssl context service is set up
+    And PushGrafanaLokiREST's success relationship is auto-terminated
+
     When all instances start up
     Then "log line 1;log line 2;log line 3" lines are published using SSL to 
the Grafana Loki server in less than 60 seconds
 
@@ -56,11 +63,14 @@ Feature: MiNiFi can publish logs to Grafana Loki server
     And a reverse proxy is set up to forward requests to the Grafana Loki 
server
     And a TailFile processor with the "File to Tail" property set to 
"/tmp/input/test_file.log"
     And a file with filename "test_file.log" and content "log line 1\nlog line 
2\nlog line 3\n" is present in "/tmp/input"
-    And a PushGrafanaLokiREST processor with the "Url" property set to 
"http://reverse-proxy-${feature_id}:3030/";
+    And a PushGrafanaLokiREST processor with the "Url" property set to 
"http://reverse-proxy-${scenario_id}:3030/";
+    And PushGrafanaLokiREST is EVENT_DRIVEN
     And the "Stream Labels" property of the PushGrafanaLokiREST processor is 
set to "job=minifi,id=docker-test"
     And the "Username" property of the PushGrafanaLokiREST processor is set to 
"admin"
     And the "Password" property of the PushGrafanaLokiREST processor is set to 
"password"
     And the "success" relationship of the TailFile processor is connected to 
the PushGrafanaLokiREST
+    And PushGrafanaLokiREST's success relationship is auto-terminated
+
     When all instances start up
     Then "log line 1;log line 2;log line 3" lines are published to the Grafana 
Loki server in less than 60 seconds
 
@@ -68,9 +78,12 @@ Feature: MiNiFi can publish logs to Grafana Loki server
     Given a Grafana Loki server is set up
     And a TailFile processor with the "File to Tail" property set to 
"/tmp/input/test_file.log"
     And a file with filename "test_file.log" and content "log line 1\nlog line 
2\nlog line 3\n" is present in "/tmp/input"
-    And a PushGrafanaLokiGrpc processor with the "Url" property set to 
"grafana-loki-server-${feature_id}:9095"
+    And a PushGrafanaLokiGrpc processor with the "Url" property set to 
"grafana-loki-server-${scenario_id}:9095"
+    And PushGrafanaLokiGrpc is EVENT_DRIVEN
     And the "Stream Labels" property of the PushGrafanaLokiGrpc processor is 
set to "job=minifi,id=docker-test"
     And the "success" relationship of the TailFile processor is connected to 
the PushGrafanaLokiGrpc
+    And PushGrafanaLokiGrpc's success relationship is auto-terminated
+
     When all instances start up
     Then "log line 1;log line 2;log line 3" lines are published to the Grafana 
Loki server in less than 60 seconds
 
@@ -78,10 +91,13 @@ Feature: MiNiFi can publish logs to Grafana Loki server
     Given a Grafana Loki server is set up with multi-tenancy enabled
     And a TailFile processor with the "File to Tail" property set to 
"/tmp/input/test_file.log"
     And a file with filename "test_file.log" and content "log line 1\nlog line 
2\nlog line 3\n" is present in "/tmp/input"
-    And a PushGrafanaLokiGrpc processor with the "Url" property set to 
"grafana-loki-server-${feature_id}:9095"
+    And a PushGrafanaLokiGrpc processor with the "Url" property set to 
"grafana-loki-server-${scenario_id}:9095"
+    And PushGrafanaLokiGrpc is EVENT_DRIVEN
     And the "Stream Labels" property of the PushGrafanaLokiGrpc processor is 
set to "job=minifi,id=docker-test"
     And the "Tenant ID" property of the PushGrafanaLokiGrpc processor is set 
to "mytenant"
     And the "success" relationship of the TailFile processor is connected to 
the PushGrafanaLokiGrpc
+    And PushGrafanaLokiGrpc's success relationship is auto-terminated
+
     When all instances start up
     Then "log line 1;log line 2;log line 3" lines are published to the 
"mytenant" tenant on the Grafana Loki server in less than 60 seconds
 
@@ -89,9 +105,13 @@ Feature: MiNiFi can publish logs to Grafana Loki server
     Given a Grafana Loki server with SSL is set up
     And a TailFile processor with the "File to Tail" property set to 
"/tmp/input/test_file.log"
     And a file with filename "test_file.log" and content "log line 1\nlog line 
2\nlog line 3\n" is present in "/tmp/input"
-    And a PushGrafanaLokiGrpc processor with the "Url" property set to 
"grafana-loki-server-${feature_id}:9095"
+    And a PushGrafanaLokiGrpc processor with the "Url" property set to 
"grafana-loki-server-${scenario_id}:9095"
+    And PushGrafanaLokiGrpc is EVENT_DRIVEN
     And the "Stream Labels" property of the PushGrafanaLokiGrpc processor is 
set to "job=minifi,id=docker-test"
+    And the "SSL Context Service" property of the PushGrafanaLokiGrpc 
processor is set to "SSLContextService"
     And the "success" relationship of the TailFile processor is connected to 
the PushGrafanaLokiGrpc
-    And a SSL context service is set up for Grafana Loki processor 
"PushGrafanaLokiGrpc"
+    And PushGrafanaLokiGrpc's success relationship is auto-terminated
+    And an ssl context service is set up
+
     When all instances start up
     Then "log line 1;log line 2;log line 3" lines are published using SSL to 
the Grafana Loki server in less than 60 seconds
diff --git 
a/extensions/grafana-loki/tests/features/resources/check_log_lines_on_grafana.py
 
b/extensions/grafana-loki/tests/features/resources/check_log_lines_on_grafana.py
new file mode 100644
index 000000000..6d961a256
--- /dev/null
+++ 
b/extensions/grafana-loki/tests/features/resources/check_log_lines_on_grafana.py
@@ -0,0 +1,79 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+import sys
+import requests
+import time
+
+
+def wait_for(action, timeout_seconds, *args, **kwargs) -> bool:
+    start_time = time.perf_counter()
+    while True:
+        result = action(*args, **kwargs)
+        if result:
+            return result
+        if timeout_seconds < (time.perf_counter() - start_time):
+            break
+        time.sleep(1)
+    return False
+
+
+def verify_log_lines_on_grafana_loki(host: str, lines: list[str], ssl: bool, 
tenant_id: str) -> bool:
+    labels = '{job="minifi"}'
+    prefix = "http://";
+    if ssl:
+        prefix = "https://";
+
+    query_url = f"{prefix}{host}:3100/loki/api/v1/query_range?query={labels}"
+
+    headers = None
+    if tenant_id:
+        headers = {'X-Scope-OrgID': tenant_id}
+
+    response = requests.get(query_url, verify=False, timeout=30, 
headers=headers)
+    if response.status_code < 200 or response.status_code >= 300:
+        return False
+
+    json_response = response.json()
+    if "data" not in json_response or "result" not in json_response["data"] or 
len(json_response["data"]["result"]) < 1:
+        return False
+
+    result = json_response["data"]["result"][0]
+    if "values" not in result:
+        return False
+
+    for line in lines:
+        if line not in str(result["values"]):
+            return False
+    return True
+
+
+def wait_for_lines_on_grafana_loki(host: str, lines: list[str], 
timeout_seconds: int, ssl: bool, tenant_id: str) -> bool:
+    return wait_for(lambda: verify_log_lines_on_grafana_loki(host, lines, ssl, 
tenant_id), timeout_seconds)
+
+
+if __name__ == "__main__":
+    if len(sys.argv) < 5:
+        sys.exit(1)
+
+    host = sys.argv[1]
+    lines = sys.argv[2]
+    timeout_seconds = int(sys.argv[3])
+    ssl = sys.argv[4].lower() == "true"
+    tenant_id = ""
+    if len(sys.argv) >= 6:
+        tenant_id = sys.argv[5]
+    if not wait_for_lines_on_grafana_loki(host, lines.split(";"), 
timeout_seconds, ssl, tenant_id):
+        sys.exit(1)
diff --git 
a/extensions/grafana-loki/tests/features/resources/reverse-proxy/Dockerfile 
b/extensions/grafana-loki/tests/features/resources/reverse-proxy/Dockerfile
new file mode 100644
index 000000000..28c25c7e0
--- /dev/null
+++ b/extensions/grafana-loki/tests/features/resources/reverse-proxy/Dockerfile
@@ -0,0 +1,8 @@
+FROM nginx:1.29.4
+
+COPY nginx.conf.template /nginx.conf.template
+COPY run.sh /run.sh
+
+RUN apt update -y && apt install -y apache2-utils && rm 
/etc/nginx/conf.d/default.conf && chmod 0755 /run.sh
+
+ENTRYPOINT [ "/run.sh" ]
diff --git 
a/extensions/grafana-loki/tests/features/resources/reverse-proxy/nginx.conf.template
 
b/extensions/grafana-loki/tests/features/resources/reverse-proxy/nginx.conf.template
new file mode 100644
index 000000000..05f8251a1
--- /dev/null
+++ 
b/extensions/grafana-loki/tests/features/resources/reverse-proxy/nginx.conf.template
@@ -0,0 +1,10 @@
+server {
+  listen 3030;
+  server_name default_server;
+
+  location / {
+    proxy_pass http://${FORWARD_HOST}:${FORWARD_PORT};
+    auth_basic "Administrator’s Area";
+    auth_basic_user_file /etc/nginx/.htpasswd;
+  }
+}
diff --git 
a/extensions/grafana-loki/tests/features/resources/reverse-proxy/run.sh 
b/extensions/grafana-loki/tests/features/resources/reverse-proxy/run.sh
new file mode 100644
index 000000000..abbd51885
--- /dev/null
+++ b/extensions/grafana-loki/tests/features/resources/reverse-proxy/run.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+htpasswd -bc /etc/nginx/.htpasswd "${BASIC_USERNAME}" "${BASIC_PASSWORD}"
+
+# shellcheck disable=SC2016
+envsubst '${FORWARD_HOST} ${FORWARD_PORT}' < /nginx.conf.template > 
/etc/nginx/conf.d/default.conf
+
+nginx -g "daemon off;"
diff --git 
a/extensions/grafana-loki/tests/features/steps/grafana_loki_container.py 
b/extensions/grafana-loki/tests/features/steps/grafana_loki_container.py
new file mode 100644
index 000000000..46fc84b5a
--- /dev/null
+++ b/extensions/grafana-loki/tests/features/steps/grafana_loki_container.py
@@ -0,0 +1,151 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+import logging
+from OpenSSL import crypto
+
+from minifi_test_framework.core.helpers import wait_for_condition, retry_check
+from minifi_test_framework.containers.container import Container
+from minifi_test_framework.containers.file import File
+from minifi_test_framework.core.minifi_test_context import MinifiTestContext
+from minifi_test_framework.core.ssl_utils import make_server_cert
+from docker.errors import ContainerError
+
+
+class GrafanaLokiOptions:
+    def __init__(self, enable_ssl: bool = False, enable_multi_tenancy: bool = 
False):
+        self.enable_ssl = enable_ssl
+        self.enable_multi_tenancy = enable_multi_tenancy
+
+
+class GrafanaLokiContainer(Container):
+    def __init__(self, test_context: MinifiTestContext, options: 
GrafanaLokiOptions):
+        super().__init__("grafana/loki:3.2.1", 
f"grafana-loki-server-{test_context.scenario_id}", test_context.network)
+        extra_ssl_settings = ""
+        if options.enable_ssl:
+            grafana_loki_cert, grafana_loki_key = 
make_server_cert(self.container_name, test_context.root_ca_cert, 
test_context.root_ca_key)
+
+            root_ca_content = 
crypto.dump_certificate(type=crypto.FILETYPE_PEM, 
cert=test_context.root_ca_cert)
+            self.files.append(File("/etc/loki/root_ca.crt", root_ca_content, 
permissions=0o644))
+
+            grafana_loki_cert_content = 
crypto.dump_certificate(type=crypto.FILETYPE_PEM, cert=grafana_loki_cert)
+            self.files.append(File("/etc/loki/cert.pem", 
grafana_loki_cert_content, permissions=0o644))
+
+            grafana_loki_key_content = 
crypto.dump_privatekey(type=crypto.FILETYPE_PEM, pkey=grafana_loki_key)
+            self.files.append(File("/etc/loki/key.pem", 
grafana_loki_key_content, permissions=0o644))
+
+            extra_ssl_settings = """
+  http_tls_config:
+    cert_file: /etc/loki/cert.pem
+    key_file: /etc/loki/key.pem
+    client_ca_file: /etc/loki/root_ca.crt
+    client_auth_type: VerifyClientCertIfGiven
+
+  grpc_tls_config:
+    cert_file: /etc/loki/cert.pem
+    key_file: /etc/loki/key.pem
+    client_ca_file: /etc/loki/root_ca.crt
+    client_auth_type: VerifyClientCertIfGiven
+
+query_scheduler:
+  grpc_client_config:
+    grpc_compression: snappy
+    tls_enabled: true
+    tls_ca_path: /etc/loki/root_ca.crt
+    tls_insecure_skip_verify: true
+
+ingester_client:
+  grpc_client_config:
+    grpc_compression: snappy
+    tls_enabled: true
+    tls_ca_path: /etc/loki/root_ca.crt
+    tls_insecure_skip_verify: true
+
+frontend:
+  grpc_client_config:
+    grpc_compression: snappy
+    tls_enabled: true
+    tls_ca_path: /etc/loki/root_ca.crt
+    tls_insecure_skip_verify: true
+
+frontend_worker:
+  grpc_client_config:
+    grpc_compression: snappy
+    tls_enabled: true
+    tls_ca_path: /etc/loki/root_ca.crt
+    tls_insecure_skip_verify: true
+"""
+
+        grafana_loki_yml_content = """
+auth_enabled: {enable_multi_tenancy}
+
+server:
+  http_listen_port: 3100
+  grpc_listen_port: 9095
+{extra_ssl_settings}
+
+common:
+  path_prefix: /loki
+  storage:
+    filesystem:
+      chunks_directory: /loki/chunks
+      rules_directory: /loki/rules
+  replication_factor: 1
+  ring:
+    kvstore:
+      store: inmemory
+
+schema_config:
+  configs:
+    - from: 2020-05-15
+      store: tsdb
+      object_store: filesystem
+      schema: v13
+      index:
+        prefix: index_
+        period: 24h
+
+ruler:
+  alertmanager_url: http://localhost:9093
+
+analytics:
+  reporting_enabled: false
+""".format(extra_ssl_settings=extra_ssl_settings, 
enable_multi_tenancy=options.enable_multi_tenancy)
+
+        self.files.append(File("/etc/loki/local-config.yaml", 
grafana_loki_yml_content.encode(), permissions=0o644))
+
+    def deploy(self):
+        super().deploy()
+        finished_str = "Loki started"
+        return wait_for_condition(
+            condition=lambda: finished_str in self.get_logs(),
+            timeout_seconds=120,
+            bail_condition=lambda: self.exited,
+            context=None)
+
+    @retry_check()
+    def are_lines_present(self, lines: str, timeout: int, ssl: bool, 
tenant_id: str = "") -> bool:
+        try:
+            self.client.containers.run("minifi-grafana-loki-helper:latest", 
["python", "/scripts/check_log_lines_on_grafana.py", self.container_name, 
lines, str(timeout), str(ssl), tenant_id],
+                                       remove=True, stdout=True, stderr=True, 
network=self.network.name)
+            return True
+        except ContainerError as e:
+            stdout = e.stdout.decode("utf-8", errors="replace") if e.stdout 
else ""
+            stderr = e.stderr.decode("utf-8", errors="replace") if e.stderr 
else ""
+            logging.error(f"Failed to run python command in grafana loki 
helper docker with error: '{e}', stdout: '{stdout}', stderr: '{stderr}'")
+            return False
+        except Exception as e:
+            logging.error(f"Unexpected error while running python command in 
grafana loki helper docker: '{e}'")
+            return False
diff --git 
a/extensions/grafana-loki/tests/features/steps/reverse_proxy_container.py 
b/extensions/grafana-loki/tests/features/steps/reverse_proxy_container.py
new file mode 100644
index 000000000..c415f6dba
--- /dev/null
+++ b/extensions/grafana-loki/tests/features/steps/reverse_proxy_container.py
@@ -0,0 +1,38 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+from minifi_test_framework.core.minifi_test_context import MinifiTestContext
+from minifi_test_framework.containers.container import Container
+from minifi_test_framework.core.helpers import wait_for_condition
+
+
+class ReverseProxyContainer(Container):
+    def __init__(self, test_context: MinifiTestContext):
+        super().__init__("minifi-reverse-proxy:latest", 
f"reverse-proxy-{test_context.scenario_id}", test_context.network)
+        self.environment = [
+            "BASIC_USERNAME=admin",
+            "BASIC_PASSWORD=password",
+            f"FORWARD_HOST=grafana-loki-server-{test_context.scenario_id}",
+            "FORWARD_PORT=3100",
+        ]
+
+    def deploy(self):
+        super().deploy()
+        finished_str = "start worker process"
+        return wait_for_condition(
+            condition=lambda: finished_str in self.get_logs(),
+            timeout_seconds=60,
+            bail_condition=lambda: self.exited,
+            context=None)
diff --git a/extensions/grafana-loki/tests/features/steps/steps.py 
b/extensions/grafana-loki/tests/features/steps/steps.py
new file mode 100644
index 000000000..bd675e110
--- /dev/null
+++ b/extensions/grafana-loki/tests/features/steps/steps.py
@@ -0,0 +1,63 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+from behave import step, then
+
+from minifi_test_framework.steps import checking_steps        # noqa: F401
+from minifi_test_framework.steps import configuration_steps   # noqa: F401
+from minifi_test_framework.steps import core_steps            # noqa: F401
+from minifi_test_framework.steps import flow_building_steps   # noqa: F401
+from minifi_test_framework.core.minifi_test_context import MinifiTestContext
+from minifi_test_framework.core.helpers import log_due_to_failure
+from grafana_loki_container import GrafanaLokiContainer, GrafanaLokiOptions
+from reverse_proxy_container import ReverseProxyContainer
+
+
+@step("a Grafana Loki server is set up")
+def step_impl(context: MinifiTestContext):
+    context.containers["grafana-loki-server"] = GrafanaLokiContainer(context, 
GrafanaLokiOptions())
+
+
+@step("a Grafana Loki server is set up with multi-tenancy enabled")
+def step_impl(context: MinifiTestContext):
+    context.containers["grafana-loki-server"] = GrafanaLokiContainer(context, 
GrafanaLokiOptions(enable_multi_tenancy=True))
+
+
+@step("a Grafana Loki server with SSL is set up")
+def step_impl(context: MinifiTestContext):
+    context.containers["grafana-loki-server"] = GrafanaLokiContainer(context, 
GrafanaLokiOptions(enable_ssl=True))
+
+
+@then("\"{lines}\" lines are published to the Grafana Loki server in less than 
{timeout_seconds:d} seconds")
+@then("\"{lines}\" line is published to the Grafana Loki server in less than 
{timeout_seconds:d} seconds")
+def step_impl(context, lines: str, timeout_seconds: int):
+    assert context.containers["grafana-loki-server"].are_lines_present(lines, 
timeout_seconds, ssl=False) or log_due_to_failure(context)
+
+
+@then("\"{lines}\" lines are published to the \"{tenant_id}\" tenant on the 
Grafana Loki server in less than {timeout_seconds:d} seconds")
+@then("\"{lines}\" line is published to the \"{tenant_id}\" tenant on the 
Grafana Loki server in less than {timeout_seconds:d} seconds")
+def step_impl(context, lines: str, timeout_seconds: int, tenant_id: str):
+    assert context.containers["grafana-loki-server"].are_lines_present(lines, 
timeout_seconds, ssl=False, tenant_id=tenant_id) or log_due_to_failure(context)
+
+
+@then("\"{lines}\" lines are published using SSL to the Grafana Loki server in 
less than {timeout_seconds:d} seconds")
+@then("\"{lines}\" line is published using SSL to the Grafana Loki server in 
less than {timeout_seconds:d} seconds")
+def step_impl(context, lines: str, timeout_seconds: int):
+    assert context.containers["grafana-loki-server"].are_lines_present(lines, 
timeout_seconds, ssl=True) or log_due_to_failure(context)
+
+
+# Nginx reverse proxy
+@step('a reverse proxy is set up to forward requests to the Grafana Loki 
server')
+def step_impl(context):
+    context.containers["reverse-proxy"] = ReverseProxyContainer(context)

Reply via email to