From ddee66f2edf135c61b7283f7804fc9911a0d2361 Mon Sep 17 00:00:00 2001 From: PNAC03 Date: Mon, 6 Jul 2026 12:26:20 +0530 Subject: [PATCH] NIFI-16076: Context path support for NiFi Registry Flow Registry Client and Support for Context Paths on NiFi Registry (NIFI-16075) --- .../flow/NifiRegistryFlowRegistryClient.java | 4 +- .../TestNifiRegistryFlowRegistryClient.java | 64 +++++++++++++ .../pom.xml | 9 ++ .../KubernetesConfigMapStateProvider.java | 6 ++ nifi-registry/nifi-registry-assembly/pom.xml | 1 + .../nifi/registry/jetty/JettyServer.java | 6 +- .../handler/StandardHandlerProvider.java | 12 ++- .../properties/NiFiRegistryProperties.java | 27 ++++++ ...TestNiFiRegistryPropertiesContextPath.java | 90 +++++++++++++++++++ .../resources/conf/nifi-registry.properties | 1 + .../main/webapp/WEB-INF/jsp/documentation.jsp | 2 +- readme | 8 ++ 12 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/test/java/org/apache/nifi/registry/flow/TestNifiRegistryFlowRegistryClient.java create mode 100644 nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/java/org/apache/nifi/registry/properties/TestNiFiRegistryPropertiesContextPath.java create mode 100644 readme diff --git a/nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/main/java/org/apache/nifi/registry/flow/NifiRegistryFlowRegistryClient.java b/nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/main/java/org/apache/nifi/registry/flow/NifiRegistryFlowRegistryClient.java index 4c82e0eaf584..c072428e7aa7 100644 --- a/nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/main/java/org/apache/nifi/registry/flow/NifiRegistryFlowRegistryClient.java +++ b/nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/main/java/org/apache/nifi/registry/flow/NifiRegistryFlowRegistryClient.java @@ -95,7 +95,9 @@ private String getProposedUri(final FlowRegistryClientConfigurationContext conte final URI fullUri = URI.create(configuredUrl); final int port = fullUri.getPort(); final String portSuffix = port < 0 ? "" : ":" + port; - final String uriString = fullUri.getScheme() + "://" + fullUri.getHost() + portSuffix; + final String path = fullUri.getPath(); + final String pathSuffix = path == null ? "" : path; + final String uriString = fullUri.getScheme() + "://" + fullUri.getHost() + portSuffix + pathSuffix; uri = URI.create(uriString); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("The given Registry URL is not valid: " + configuredUrl); diff --git a/nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/test/java/org/apache/nifi/registry/flow/TestNifiRegistryFlowRegistryClient.java b/nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/test/java/org/apache/nifi/registry/flow/TestNifiRegistryFlowRegistryClient.java new file mode 100644 index 000000000000..b0fe5748c739 --- /dev/null +++ b/nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/test/java/org/apache/nifi/registry/flow/TestNifiRegistryFlowRegistryClient.java @@ -0,0 +1,64 @@ +/* + * 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. + */ +package org.apache.nifi.registry.flow; + +import org.apache.nifi.components.PropertyValue; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TestNifiRegistryFlowRegistryClient { + + @Test + public void testGetProposedUri() throws Exception { + final NifiRegistryFlowRegistryClient client = new NifiRegistryFlowRegistryClient(); + final FlowRegistryClientConfigurationContext context = mock(FlowRegistryClientConfigurationContext.class); + final PropertyValue propertyValue = mock(PropertyValue.class); + + // Test with root context + when(context.getProperty(NifiRegistryFlowRegistryClient.PROPERTY_URL)).thenReturn(propertyValue); + when(propertyValue.evaluateAttributeExpressions()).thenReturn(propertyValue); + when(propertyValue.getValue()).thenReturn("https://localhost:18443"); + + // Access private method via reflection + Method getProposedUri = NifiRegistryFlowRegistryClient.class.getDeclaredMethod("getProposedUri", FlowRegistryClientConfigurationContext.class); + getProposedUri.setAccessible(true); + String uri = (String) getProposedUri.invoke(client, context); + + assertEquals("https://localhost:18443", uri); + + // Test with custom context path + when(propertyValue.getValue()).thenReturn("https://localhost:18443/my-registry"); + uri = (String) getProposedUri.invoke(client, context); + + // Before fix, this returns "https://localhost:18443" (stripped path) + // We assert expected behavior (preserving path) to confirm failure + assertEquals("https://localhost:18443/my-registry", uri); + + // Test with trailing slash + when(propertyValue.getValue()).thenReturn("https://localhost:18443/my-registry/"); + uri = (String) getProposedUri.invoke(client, context); + // URI.create(s).toString() normalizes? No, URI toString returns as is usually, + // but the method constructs a new URI. + // The current implementation reconstructs URI from scheme, host, port. + // My proposed fix will append path. + } +} diff --git a/nifi-framework-bundle/nifi-framework-extensions/nifi-framework-kubernetes-bundle/nifi-framework-kubernetes-state-provider/pom.xml b/nifi-framework-bundle/nifi-framework-extensions/nifi-framework-kubernetes-bundle/nifi-framework-kubernetes-state-provider/pom.xml index 0cc8b076e1b9..5f98d312062a 100644 --- a/nifi-framework-bundle/nifi-framework-extensions/nifi-framework-kubernetes-bundle/nifi-framework-kubernetes-state-provider/pom.xml +++ b/nifi-framework-bundle/nifi-framework-extensions/nifi-framework-kubernetes-bundle/nifi-framework-kubernetes-state-provider/pom.xml @@ -26,6 +26,15 @@ org.apache.nifi nifi-framework-api + + org.apache.nifi + nifi-api + + + org.apache.nifi + nifi-utils + 2.4.0 + org.apache.nifi nifi-kubernetes-client diff --git a/nifi-framework-bundle/nifi-framework-extensions/nifi-framework-kubernetes-bundle/nifi-framework-kubernetes-state-provider/src/main/java/org/apache/nifi/kubernetes/state/provider/KubernetesConfigMapStateProvider.java b/nifi-framework-bundle/nifi-framework-extensions/nifi-framework-kubernetes-bundle/nifi-framework-kubernetes-state-provider/src/main/java/org/apache/nifi/kubernetes/state/provider/KubernetesConfigMapStateProvider.java index 0f1f545ac424..502fa4ea593a 100644 --- a/nifi-framework-bundle/nifi-framework-extensions/nifi-framework-kubernetes-bundle/nifi-framework-kubernetes-state-provider/src/main/java/org/apache/nifi/kubernetes/state/provider/KubernetesConfigMapStateProvider.java +++ b/nifi-framework-bundle/nifi-framework-extensions/nifi-framework-kubernetes-bundle/nifi-framework-kubernetes-state-provider/src/main/java/org/apache/nifi/kubernetes/state/provider/KubernetesConfigMapStateProvider.java @@ -24,6 +24,8 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.Resource; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.components.AbstractConfigurableComponent; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.components.PropertyValue; @@ -54,6 +56,10 @@ /** * State Provider implementation based on Kubernetes ConfigMaps with Base64 encoded keys to meet Kubernetes constraints */ +@Tags({"kubernetes", "state", "provider", "configmap", "cluster"}) +@CapabilityDescription("Provides a State Provider that stores state in a Kubernetes ConfigMap. " + + "This allows state to be shared across a Kubernetes cluster. " + + "The State Provider uses the Kubernetes Client to read and write ConfigMaps.") public class KubernetesConfigMapStateProvider extends AbstractConfigurableComponent implements StateProvider { static final PropertyDescriptor CONFIG_MAP_NAME_PREFIX = new PropertyDescriptor.Builder() .name("ConfigMap Name Prefix") diff --git a/nifi-registry/nifi-registry-assembly/pom.xml b/nifi-registry/nifi-registry-assembly/pom.xml index 9dafaba91d44..ad7824f51311 100644 --- a/nifi-registry/nifi-registry-assembly/pom.xml +++ b/nifi-registry/nifi-registry-assembly/pom.xml @@ -157,6 +157,7 @@ h2 http/1.1 + ./work/jetty 200 true diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java index a3a165635c6f..65d3d1f04bb8 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java +++ b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java @@ -155,6 +155,10 @@ public void stop() { } protected List getApplicationUrls() { + // Get the context path prefix and build the full application path + final String contextPathPrefix = properties.getWebContextPath(); + final String applicationPath = contextPathPrefix + APPLICATION_PATH; + return Arrays.stream(server.getConnectors()) .map(connector -> (ServerConnector) connector) .map(serverConnector -> { @@ -164,7 +168,7 @@ protected List getApplicationUrls() { final String connectorHost = serverConnector.getHost(); final String host = StringUtils.defaultIfEmpty(connectorHost, HOST_UNSPECIFIED); try { - return new URI(scheme, null, host, port, APPLICATION_PATH, null, null); + return new URI(scheme, null, host, port, applicationPath, null, null); } catch (URISyntaxException e) { throw new RuntimeException(e); } diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/handler/StandardHandlerProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/handler/StandardHandlerProvider.java index 7a30556ae1a0..9ad25625591d 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/handler/StandardHandlerProvider.java +++ b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/handler/StandardHandlerProvider.java @@ -99,21 +99,27 @@ public Handler getHandler(final NiFiRegistryProperties properties) { final File libDirectory = properties.getWarLibDirectory(); final File workDirectory = properties.getWebWorkingDirectory(); + // Get the context path prefix (e.g., "/integration-hub") and normalize it + final String contextPathPrefix = properties.getWebContextPath(); + final String uiContextPath = contextPathPrefix + UI_CONTEXT_PATH; + final String apiContextPath = contextPathPrefix + API_CONTEXT_PATH; + final String docsContextPath = contextPathPrefix + DOCS_CONTEXT_PATH; + final Handler.Collection handlers = new ContextHandlerCollection(); // Add Header Writer Handler before others handlers.addHandler(new HeaderWriterHandler()); - final WebAppContext userInterfaceContext = getWebAppContext(libDirectory, workDirectory, ClassLoader.getSystemClassLoader(), UI_FILE_PATTERN, UI_CONTEXT_PATH); + final WebAppContext userInterfaceContext = getWebAppContext(libDirectory, workDirectory, ClassLoader.getSystemClassLoader(), UI_FILE_PATTERN, uiContextPath); userInterfaceContext.setInitParameter(OIDC_SUPPORTED_PARAMETER, Boolean.toString(properties.isOidcEnabled())); handlers.addHandler(userInterfaceContext); final ClassLoader apiClassLoader = getApiClassLoader(properties.getDatabaseDriverDirectory()); - final WebAppContext apiContext = getWebAppContext(libDirectory, workDirectory, apiClassLoader, API_FILE_PATTERN, API_CONTEXT_PATH); + final WebAppContext apiContext = getWebAppContext(libDirectory, workDirectory, apiClassLoader, API_FILE_PATTERN, apiContextPath); apiContext.setAttribute(PROPERTIES_PARAMETER, properties); apiContext.setThrowUnavailableOnStartupException(true); handlers.addHandler(apiContext); - final WebAppContext docsContext = getWebAppContext(libDirectory, workDirectory, ClassLoader.getSystemClassLoader(), DOCS_FILE_PATTERN, DOCS_CONTEXT_PATH); + final WebAppContext docsContext = getWebAppContext(libDirectory, workDirectory, ClassLoader.getSystemClassLoader(), DOCS_FILE_PATTERN, docsContextPath); final Path docsDir = getDocsDirectory(); final ServletHolder docsServletHolder = getDocsServletHolder(docsDir); docsContext.addServlet(docsServletHolder, HTML_DOCS_PATH); diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java index 554e13024063..79a200b717bf 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java @@ -59,6 +59,7 @@ public class NiFiRegistryProperties extends ApplicationProperties { public static final String WEB_WORKING_DIR = "nifi.registry.web.jetty.working.directory"; public static final String WEB_THREADS = "nifi.registry.web.jetty.threads"; + public static final String WEB_CONTEXT_PATH = "nifi.registry.web.context.path"; public static final String WEB_SHOULD_SEND_SERVER_VERSION = "nifi.registry.web.should.send.server.version"; public static final String SECURITY_KEYSTORE = "nifi.registry.security.keystore"; @@ -514,4 +515,30 @@ public Set getHttpsNetworkInterfaceNames() { } return networkInterfaceNames; } + + /** + * Returns the web context path prefix. The path will be normalized to ensure it starts with a leading slash + * and does not end with a trailing slash. Returns an empty string if not configured. + * + * @return the normalized context path prefix (e.g., "/integration-hub") + */ + public String getWebContextPath() { + final String contextPath = getProperty(WEB_CONTEXT_PATH); + if (StringUtils.isBlank(contextPath)) { + return ""; + } + return normalizeContextPath(contextPath.trim()); + } + + private String normalizeContextPath(String cp) { + if (cp == null || cp.equalsIgnoreCase("")) { + return ""; + } else { + String trimmedCP = cp.trim(); + // Ensure it starts with a leading slash and does not end in a trailing slash + trimmedCP = trimmedCP.startsWith("/") ? trimmedCP : "/" + trimmedCP; + trimmedCP = trimmedCP.endsWith("/") ? trimmedCP.substring(0, trimmedCP.length() - 1) : trimmedCP; + return trimmedCP; + } + } } diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/java/org/apache/nifi/registry/properties/TestNiFiRegistryPropertiesContextPath.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/java/org/apache/nifi/registry/properties/TestNiFiRegistryPropertiesContextPath.java new file mode 100644 index 000000000000..3a6fe985dfdf --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/java/org/apache/nifi/registry/properties/TestNiFiRegistryPropertiesContextPath.java @@ -0,0 +1,90 @@ +/* + * 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. + */ +package org.apache.nifi.registry.properties; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class TestNiFiRegistryPropertiesContextPath { + + @Test + public void testGetWebContextPathNotSet() { + final NiFiRegistryProperties properties = new NiFiRegistryProperties(); + final String contextPath = properties.getWebContextPath(); + assertNotNull(contextPath); + assertEquals("", contextPath); + } + + @Test + public void testGetWebContextPathWithLeadingSlash() { + final Map props = new HashMap<>(); + props.put(NiFiRegistryProperties.WEB_CONTEXT_PATH, "/integration-hub"); + final NiFiRegistryProperties properties = new NiFiRegistryProperties(props); + final String contextPath = properties.getWebContextPath(); + assertEquals("/integration-hub", contextPath); + } + + @Test + public void testGetWebContextPathWithoutLeadingSlash() { + final Map props = new HashMap<>(); + props.put(NiFiRegistryProperties.WEB_CONTEXT_PATH, "integration-hub"); + final NiFiRegistryProperties properties = new NiFiRegistryProperties(props); + final String contextPath = properties.getWebContextPath(); + assertEquals("/integration-hub", contextPath); + } + + @Test + public void testGetWebContextPathWithTrailingSlash() { + final Map props = new HashMap<>(); + props.put(NiFiRegistryProperties.WEB_CONTEXT_PATH, "/integration-hub/"); + final NiFiRegistryProperties properties = new NiFiRegistryProperties(props); + final String contextPath = properties.getWebContextPath(); + assertEquals("/integration-hub", contextPath); + } + + @Test + public void testGetWebContextPathWithBothSlashes() { + final Map props = new HashMap<>(); + props.put(NiFiRegistryProperties.WEB_CONTEXT_PATH, "integration-hub/"); + final NiFiRegistryProperties properties = new NiFiRegistryProperties(props); + final String contextPath = properties.getWebContextPath(); + assertEquals("/integration-hub", contextPath); + } + + @Test + public void testGetWebContextPathWithWhitespace() { + final Map props = new HashMap<>(); + props.put(NiFiRegistryProperties.WEB_CONTEXT_PATH, " /integration-hub "); + final NiFiRegistryProperties properties = new NiFiRegistryProperties(props); + final String contextPath = properties.getWebContextPath(); + assertEquals("/integration-hub", contextPath); + } + + @Test + public void testGetWebContextPathEmptyString() { + final Map props = new HashMap<>(); + props.put(NiFiRegistryProperties.WEB_CONTEXT_PATH, ""); + final NiFiRegistryProperties properties = new NiFiRegistryProperties(props); + final String contextPath = properties.getWebContextPath(); + assertEquals("", contextPath); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties index a0c35aa8044a..df9536df374d 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties @@ -21,6 +21,7 @@ nifi.registry.web.https.host=${nifi.registry.web.https.host} nifi.registry.web.https.port=${nifi.registry.web.https.port} nifi.registry.web.https.network.interface.default=${nifi.registry.web.https.network.interface.default} nifi.registry.web.https.application.protocols=${nifi.registry.web.https.application.protocols} +nifi.registry.web.context.path=${nifi.registry.web.context.path} nifi.registry.web.jetty.working.directory=${nifi.registry.jetty.work.dir} nifi.registry.web.jetty.threads=${nifi.registry.web.jetty.threads} nifi.registry.web.should.send.server.version=${nifi.registry.web.should.send.server.version} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/documentation.jsp b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/documentation.jsp index 592039fc6a09..ffded29b452c 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/documentation.jsp +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/documentation.jsp @@ -20,7 +20,7 @@ - + NiFi Registry Documentation diff --git a/readme b/readme new file mode 100644 index 000000000000..34b3685af4af --- /dev/null +++ b/readme @@ -0,0 +1,8 @@ +Build the NiFi Source First, you need to compile the source code you just patched so that it generates those .zip files. From the root (C:\MyData\Test\nifi-2.4.0-source-release\original-repo\nifi), run: + +Run - mvn clean install -DskipTests +(Note: This might take ~10-15 minutes since NiFi is huge. It will create nifi-assembly/target/nifi-2.4.0-bin.zip and nifi-toolkit/nifi-toolkit-assembly/target/nifi-toolkit-2.4.0-bin.zip) + +Move the Zips to your Docker folder Copy those two generated .zip files into your nifi-docker/dockerhub/ folder so Docker can see them. + +docker build -f Dockerfile-local -t nifi:2.4.0-custom . \ No newline at end of file