NIFI-13665 Refactored Bootstrap and Runtime Process Handling (#9192)

- Added BootstrapProcess main class to nifi-bootstrap
- Added HTTP Management Server to nifi-runtime for status
- Added HTTP client to nifi-bootstrap for status
- Removed TCP socket listeners from nifi-bootstrap and nifi-runtime
- Removed dump and env commands
- Removed java property from bootstrap.conf
- Removed nifi.bootstrap.listen.port from bootstrap.conf
This commit is contained in:
David Handermann 2024-08-26 13:22:04 -05:00 committed by GitHub
parent 7945234f5a
commit 6b6e8d6f78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 4102 additions and 2582 deletions

View File

@ -22,7 +22,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import org.apache.nifi.util.LimitingInputStream;
public class BootstrapRequestReader {
private final String secretKey;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.util;
package org.apache.nifi.minifi.bootstrap;
import java.io.IOException;
import java.io.InputStream;

View File

@ -24,20 +24,17 @@ language governing permissions and limitations under the License. -->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-cert-builder</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<!-- Referenced in org.apache.nifi.NiFi -->
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-properties-loader</artifactId>
<version>2.0.0-SNAPSHOT</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,106 +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.
*/
package org.apache.nifi.bootstrap;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Arrays;
public class BootstrapCodec {
private final RunNiFi runner;
private final BufferedReader reader;
private final BufferedWriter writer;
public BootstrapCodec(final RunNiFi runner, final InputStream in, final OutputStream out) {
this.runner = runner;
this.reader = new BufferedReader(new InputStreamReader(in));
this.writer = new BufferedWriter(new OutputStreamWriter(out));
}
public void communicate() throws IOException {
final String line = reader.readLine();
final String[] splits = line.split(" ");
if (splits.length < 0) {
throw new IOException("Received invalid command from NiFi: " + line);
}
final String cmd = splits[0];
final String[] args;
if (splits.length == 1) {
args = new String[0];
} else {
args = Arrays.copyOfRange(splits, 1, splits.length);
}
try {
processRequest(cmd, args);
} catch (final InvalidCommandException ice) {
throw new IOException("Received invalid command from NiFi: " + line + (ice.getMessage() == null ? "" : " - Details: " + ice.toString()));
}
}
private void processRequest(final String cmd, final String[] args) throws InvalidCommandException, IOException {
switch (cmd) {
case "PORT": {
if (args.length != 2) {
throw new InvalidCommandException();
}
final int port;
try {
port = Integer.parseInt(args[0]);
} catch (final NumberFormatException nfe) {
throw new InvalidCommandException("Invalid Port number; should be integer between 1 and 65535");
}
if (port < 1 || port > 65535) {
throw new InvalidCommandException("Invalid Port number; should be integer between 1 and 65535");
}
final String secretKey = args[1];
runner.setNiFiCommandControlPort(port, secretKey);
writer.write("OK");
writer.newLine();
writer.flush();
}
break;
case "STARTED": {
if (args.length != 1) {
throw new InvalidCommandException("STARTED command must contain a status argument");
}
if (!"true".equals(args[0]) && !"false".equals(args[0])) {
throw new InvalidCommandException("Invalid status for STARTED command; should be true or false, but was '" + args[0] + "'");
}
final boolean started = Boolean.parseBoolean(args[0]);
runner.setNiFiStarted(started);
writer.write("OK");
writer.newLine();
writer.flush();
}
break;
}
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.bootstrap;
import org.apache.nifi.bootstrap.command.BootstrapCommand;
import org.apache.nifi.bootstrap.command.BootstrapCommandProvider;
import org.apache.nifi.bootstrap.command.CommandStatus;
import org.apache.nifi.bootstrap.command.StandardBootstrapCommandProvider;
import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Bootstrap Process responsible for reading configuration and maintaining application status
*/
public class BootstrapProcess {
/**
* Start Application
*
* @param arguments Array of arguments
*/
public static void main(final String[] arguments) {
final BootstrapCommandProvider bootstrapCommandProvider = new StandardBootstrapCommandProvider();
final BootstrapCommand bootstrapCommand = bootstrapCommandProvider.getBootstrapCommand(arguments);
run(bootstrapCommand);
}
private static void run(final BootstrapCommand bootstrapCommand) {
bootstrapCommand.run();
final CommandStatus commandStatus = bootstrapCommand.getCommandStatus();
if (CommandStatus.RUNNING == commandStatus) {
final Logger logger = LoggerFactory.getLogger(ApplicationClassName.BOOTSTRAP_COMMAND.getName());
logger.info("Bootstrap Process Running");
} else {
final int status = commandStatus.getStatus();
System.exit(status);
}
}
}

View File

@ -1,141 +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.
*/
package org.apache.nifi.bootstrap;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import org.apache.nifi.bootstrap.util.LimitingInputStream;
public class NiFiListener {
private ServerSocket serverSocket;
private volatile Listener listener;
int start(final RunNiFi runner, final int listenPort) throws IOException {
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("localhost", listenPort));
final int localPort = serverSocket.getLocalPort();
listener = new Listener(serverSocket, runner);
final Thread listenThread = new Thread(listener);
listenThread.setName("Listen to NiFi");
listenThread.setDaemon(true);
listenThread.start();
return localPort;
}
public void stop() throws IOException {
final Listener listener = this.listener;
if (listener == null) {
return;
}
listener.stop();
}
private class Listener implements Runnable {
private final ServerSocket serverSocket;
private final ExecutorService executor;
private final RunNiFi runner;
private volatile boolean stopped = false;
public Listener(final ServerSocket serverSocket, final RunNiFi runner) {
this.serverSocket = serverSocket;
this.executor = Executors.newFixedThreadPool(2, new ThreadFactory() {
@Override
public Thread newThread(final Runnable runnable) {
final Thread t = Executors.defaultThreadFactory().newThread(runnable);
t.setDaemon(true);
t.setName("NiFi Bootstrap Command Listener");
return t;
}
});
this.runner = runner;
}
public void stop() throws IOException {
stopped = true;
executor.shutdown();
try {
executor.awaitTermination(3, TimeUnit.SECONDS);
} catch (final InterruptedException ie) {
}
serverSocket.close();
}
@Override
public void run() {
while (!serverSocket.isClosed()) {
try {
if (stopped) {
return;
}
final Socket socket;
try {
socket = serverSocket.accept();
} catch (final IOException ioe) {
if (stopped) {
return;
}
throw ioe;
}
executor.submit(new Runnable() {
@Override
public void run() {
try {
// we want to ensure that we don't try to read data from an InputStream directly
// by a BufferedReader because any user on the system could open a socket and send
// a multi-gigabyte file without any new lines in order to crash the Bootstrap,
// which in turn may cause the Shutdown Hook to shutdown NiFi.
// So we will limit the amount of data to read to 4 KB
final InputStream limitingIn = new LimitingInputStream(socket.getInputStream(), 4096);
final BootstrapCodec codec = new BootstrapCodec(runner, limitingIn, socket.getOutputStream());
codec.communicate();
} catch (final Throwable t) {
System.out.println("Failed to communicate with NiFi due to " + t);
t.printStackTrace();
} finally {
try {
socket.close();
} catch (final IOException ioe) {
}
}
}
});
} catch (final Throwable t) {
System.err.println("Failed to receive information from NiFi due to " + t);
t.printStackTrace();
}
}
}
}
}

View File

@ -1,98 +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.
*/
package org.apache.nifi.bootstrap;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
public class ShutdownHook extends Thread {
private final Process nifiProcess;
private final Long pid;
private final RunNiFi runner;
private final int gracefulShutdownSeconds;
private final ExecutorService executor;
private volatile String secretKey;
public ShutdownHook(final Process nifiProcess, final Long pid, final RunNiFi runner, final String secretKey, final int gracefulShutdownSeconds, final ExecutorService executor) {
this.nifiProcess = nifiProcess;
this.pid = pid;
this.runner = runner;
this.secretKey = secretKey;
this.gracefulShutdownSeconds = gracefulShutdownSeconds;
this.executor = executor;
}
void setSecretKey(final String secretKey) {
this.secretKey = secretKey;
}
@Override
public void run() {
executor.shutdown();
runner.setAutoRestartNiFi(false);
final int ccPort = runner.getNiFiCommandControlPort();
if (ccPort > 0) {
System.out.printf("NiFi PID [%d] shutdown started%n", pid);
try {
final Socket socket = new Socket("localhost", ccPort);
final OutputStream out = socket.getOutputStream();
out.write(("SHUTDOWN " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
out.flush();
socket.close();
} catch (final IOException ioe) {
System.out.println("Failed to Shutdown NiFi due to " + ioe);
}
}
System.out.printf("NiFi PID [%d] shutdown in progress...%n", pid);
final long startWait = System.nanoTime();
while (RunNiFi.isAlive(nifiProcess)) {
final long waitNanos = System.nanoTime() - startWait;
final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos);
if (waitSeconds >= gracefulShutdownSeconds && gracefulShutdownSeconds > 0) {
if (RunNiFi.isAlive(nifiProcess)) {
System.out.println("NiFi has not finished shutting down after " + gracefulShutdownSeconds + " seconds. Killing process.");
nifiProcess.destroy();
}
break;
} else {
try {
Thread.sleep(1000L);
} catch (final InterruptedException ie) {
}
}
}
try {
final File statusFile = runner.getStatusFile();
if (!statusFile.delete()) {
System.err.println("Failed to delete status file " + statusFile.getAbsolutePath() + "; this file should be cleaned up manually");
}
} catch (IOException ex) {
System.err.println("Failed to retrieve status file " + ex);
}
}
}

View File

@ -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.
*/
package org.apache.nifi.bootstrap.command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
import java.util.Optional;
/**
* Bootstrap Command to return the status of the application process as a child of the bootstrap process
*/
class ApplicationProcessStatusBootstrapCommand implements BootstrapCommand {
private static final Logger logger = LoggerFactory.getLogger(ApplicationProcessStatusBootstrapCommand.class);
private final ProcessHandle processHandle;
private CommandStatus commandStatus = CommandStatus.ERROR;
ApplicationProcessStatusBootstrapCommand(final ProcessHandle processHandle) {
this.processHandle = Objects.requireNonNull(processHandle);
}
@Override
public CommandStatus getCommandStatus() {
return commandStatus;
}
@Override
public void run() {
final Optional<ProcessHandle> childProcessHandleFound = processHandle.children().findFirst();
if (childProcessHandleFound.isEmpty()) {
logger.info("Application Process not found");
commandStatus = CommandStatus.STOPPED;
} else {
final ProcessHandle childProcessHandle = childProcessHandleFound.get();
if (childProcessHandle.isAlive()) {
logger.info("Application Process [{}] running", childProcessHandle.pid());
commandStatus = CommandStatus.SUCCESS;
} else {
logger.info("Application Process [{}] stopped", childProcessHandle.pid());
commandStatus = CommandStatus.COMMUNICATION_FAILED;
}
}
}
}

View File

@ -0,0 +1,29 @@
/*
* 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.bootstrap.command;
/**
* Bootstrap Command extension of Runnable with command status
*/
public interface BootstrapCommand extends Runnable {
/**
* Get Command Status on completion
*
* @return Command Status
*/
CommandStatus getCommandStatus();
}

View File

@ -0,0 +1,30 @@
/*
* 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.bootstrap.command;
/**
* Abstraction for parsing arguments and returning runnable Bootstrap Command
*/
public interface BootstrapCommandProvider {
/**
* Get Bootstrap Command
*
* @param arguments Application arguments
* @return Bootstrap Command to run
*/
BootstrapCommand getBootstrapCommand(String[] arguments);
}

View File

@ -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.
*/
package org.apache.nifi.bootstrap.command;
/**
* Enumeration of Bootstrap Command Statuses with status codes
*/
public enum CommandStatus {
RUNNING(-1),
SUCCESS(0),
ERROR(1),
STOPPED(3),
COMMUNICATION_FAILED(4),
FAILED(5);
private final int status;
CommandStatus(final int status) {
this.status = status;
}
/**
* Get Status Code for use with System.exit()
*
* @return Status Code
*/
public int getStatus() {
return status;
}
}

View File

@ -0,0 +1,91 @@
/*
* 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.bootstrap.command;
import org.apache.nifi.bootstrap.command.process.ManagementServerAddressProvider;
import org.apache.nifi.bootstrap.command.process.ProcessBuilderProvider;
import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
import org.apache.nifi.bootstrap.command.process.StandardManagementServerAddressProvider;
import org.apache.nifi.bootstrap.command.process.StandardProcessBuilderProvider;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.apache.nifi.bootstrap.property.ApplicationPropertyHandler;
import org.apache.nifi.bootstrap.property.SecurityApplicationPropertyHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.PrintStream;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Bootstrap Command to get the command arguments for running the application
*/
class GetRunCommandBootstrapCommand implements BootstrapCommand {
private static final String SPACE_SEPARATOR = " ";
private static final Logger logger = LoggerFactory.getLogger(GetRunCommandBootstrapCommand.class);
private final ConfigurationProvider configurationProvider;
private final ProcessHandleProvider processHandleProvider;
private final ManagementServerAddressProvider managementServerAddressProvider;
private final PrintStream outputStream;
private CommandStatus commandStatus = CommandStatus.ERROR;
public GetRunCommandBootstrapCommand(final ConfigurationProvider configurationProvider, final ProcessHandleProvider processHandleProvider, final PrintStream outputStream) {
this.configurationProvider = Objects.requireNonNull(configurationProvider);
this.processHandleProvider = Objects.requireNonNull(processHandleProvider);
this.outputStream = Objects.requireNonNull(outputStream);
this.managementServerAddressProvider = new StandardManagementServerAddressProvider(configurationProvider);
}
@Override
public CommandStatus getCommandStatus() {
return commandStatus;
}
@Override
public void run() {
try {
final Optional<ProcessHandle> applicationProcessHandle = processHandleProvider.findApplicationProcessHandle();
if (applicationProcessHandle.isEmpty()) {
final ApplicationPropertyHandler securityApplicationPropertyHandler = new SecurityApplicationPropertyHandler(logger);
securityApplicationPropertyHandler.handleProperties(configurationProvider.getApplicationProperties());
final ProcessBuilderProvider processBuilderProvider = new StandardProcessBuilderProvider(configurationProvider, managementServerAddressProvider);
final ProcessBuilder processBuilder = processBuilderProvider.getApplicationProcessBuilder();
final List<String> command = processBuilder.command();
final String processCommand = String.join(SPACE_SEPARATOR, command);
outputStream.println(processCommand);
commandStatus = CommandStatus.SUCCESS;
} else {
logger.info("Application Process [{}] running", applicationProcessHandle.get().pid());
commandStatus = CommandStatus.ERROR;
}
} catch (final Throwable e) {
logger.warn("Application Process command building failed", e);
commandStatus = CommandStatus.FAILED;
}
}
}

View File

@ -0,0 +1,178 @@
/*
* 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.bootstrap.command;
import org.apache.nifi.bootstrap.command.io.HttpRequestMethod;
import org.apache.nifi.bootstrap.command.io.ResponseStreamHandler;
import org.apache.nifi.bootstrap.command.process.ManagementServerAddressProvider;
import org.apache.nifi.bootstrap.command.process.ProcessHandleManagementServerAddressProvider;
import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
import org.apache.nifi.bootstrap.configuration.ManagementServerPath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
/**
* Sequence of Bootstrap Commands
*/
class ManagementServerBootstrapCommand implements BootstrapCommand {
private static final Logger commandLogger = LoggerFactory.getLogger(ApplicationClassName.BOOTSTRAP_COMMAND.getName());
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
private static final Duration READ_TIMEOUT = Duration.ofSeconds(15);
private static final String SERVER_URI = "http://%s%s";
private static final char QUERY_SEPARATOR = '?';
private final ProcessHandleProvider processHandleProvider;
private final HttpRequestMethod httpRequestMethod;
private final ManagementServerPath managementServerPath;
private final String managementServerQuery;
private final int successStatusCode;
private final ResponseStreamHandler responseStreamHandler;
private CommandStatus commandStatus = CommandStatus.ERROR;
ManagementServerBootstrapCommand(
final ProcessHandleProvider processHandleProvider,
final ManagementServerPath managementServerPath,
final ResponseStreamHandler responseStreamHandler
) {
this(processHandleProvider, HttpRequestMethod.GET, managementServerPath, null, HttpURLConnection.HTTP_OK, responseStreamHandler);
}
ManagementServerBootstrapCommand(
final ProcessHandleProvider processHandleProvider,
final HttpRequestMethod httpRequestMethod,
final ManagementServerPath managementServerPath,
final String managementServerQuery,
final int successStatusCode,
final ResponseStreamHandler responseStreamHandler
) {
this.processHandleProvider = Objects.requireNonNull(processHandleProvider);
this.httpRequestMethod = Objects.requireNonNull(httpRequestMethod);
this.managementServerPath = Objects.requireNonNull(managementServerPath);
this.managementServerQuery = managementServerQuery;
this.successStatusCode = successStatusCode;
this.responseStreamHandler = Objects.requireNonNull(responseStreamHandler);
}
@Override
public CommandStatus getCommandStatus() {
return commandStatus;
}
@Override
public void run() {
final Optional<ProcessHandle> applicationProcessHandle = processHandleProvider.findApplicationProcessHandle();
if (applicationProcessHandle.isEmpty()) {
commandStatus = CommandStatus.STOPPED;
getCommandLogger().info("Application Process STOPPED");
} else {
run(applicationProcessHandle.get());
}
}
protected void run(final ProcessHandle applicationProcessHandle) {
final ManagementServerAddressProvider managementServerAddressProvider = new ProcessHandleManagementServerAddressProvider(applicationProcessHandle);
final Optional<String> managementServerAddress = managementServerAddressProvider.getAddress();
final long pid = applicationProcessHandle.pid();
if (managementServerAddress.isEmpty()) {
getCommandLogger().info("Application Process [{}] Management Server address not found", pid);
commandStatus = CommandStatus.ERROR;
} else {
final URI managementServerUri = getManagementServerUri(managementServerAddress.get());
try (HttpClient httpClient = getHttpClient()) {
final HttpRequest httpRequest = getHttpRequest(managementServerUri);
final HttpResponse<InputStream> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofInputStream());
final int statusCode = response.statusCode();
try (InputStream responseStream = response.body()) {
onResponseStatus(applicationProcessHandle, statusCode, responseStream);
}
} catch (final Exception e) {
commandStatus = CommandStatus.COMMUNICATION_FAILED;
getCommandLogger().info("Application Process [{}] Management Server [{}] communication failed", pid, managementServerUri);
}
}
}
protected void onResponseStatus(final ProcessHandle applicationProcessHandle, final int statusCode, final InputStream responseStream) {
final long pid = applicationProcessHandle.pid();
if (successStatusCode == statusCode) {
commandStatus = CommandStatus.SUCCESS;
getCommandLogger().info("Application Process [{}] Command Status [{}] HTTP {}", pid, commandStatus, statusCode);
responseStreamHandler.onResponseStream(responseStream);
} else {
commandStatus = CommandStatus.COMMUNICATION_FAILED;
getCommandLogger().warn("Application Process [{}] Command Status [{}] HTTP {}", pid, commandStatus, statusCode);
}
}
protected Logger getCommandLogger() {
return commandLogger;
}
protected HttpRequest getHttpRequest(final URI managementServerUri) {
return HttpRequest.newBuilder()
.method(httpRequestMethod.name(), HttpRequest.BodyPublishers.noBody())
.uri(managementServerUri)
.timeout(READ_TIMEOUT)
.build();
}
protected URI getManagementServerUri(final String managementServerAddress) {
final StringBuilder builder = new StringBuilder();
final String serverUri = SERVER_URI.formatted(managementServerAddress, managementServerPath.getPath());
builder.append(serverUri);
if (managementServerQuery != null) {
builder.append(QUERY_SEPARATOR);
builder.append(managementServerQuery);
}
return URI.create(builder.toString());
}
protected HttpClient getHttpClient() {
final HttpClient.Builder builder = HttpClient.newBuilder();
builder.connectTimeout(CONNECT_TIMEOUT);
return builder.build();
}
}

View File

@ -0,0 +1,122 @@
/*
* 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.bootstrap.command;
import com.sun.management.UnixOperatingSystemMXBean;
import org.apache.nifi.bootstrap.command.process.ManagementServerAddressProvider;
import org.apache.nifi.bootstrap.command.process.ProcessBuilderProvider;
import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
import org.apache.nifi.bootstrap.command.process.StandardManagementServerAddressProvider;
import org.apache.nifi.bootstrap.command.process.StandardProcessBuilderProvider;
import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.apache.nifi.bootstrap.process.RuntimeValidatorExecutor;
import org.apache.nifi.bootstrap.property.ApplicationPropertyHandler;
import org.apache.nifi.bootstrap.property.SecurityApplicationPropertyHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.util.Objects;
import java.util.Optional;
/**
* Bootstrap Command to run the application
*/
class RunBootstrapCommand implements BootstrapCommand {
private static final String SPACE_SEPARATOR = " ";
private static final Logger commandLogger = LoggerFactory.getLogger(ApplicationClassName.BOOTSTRAP_COMMAND.getName());
private static final Logger logger = LoggerFactory.getLogger(RunBootstrapCommand.class);
private static final RuntimeValidatorExecutor runtimeValidatorExecutor = new RuntimeValidatorExecutor();
private final ConfigurationProvider configurationProvider;
private final ProcessHandleProvider processHandleProvider;
private final ManagementServerAddressProvider managementServerAddressProvider;
private CommandStatus commandStatus = CommandStatus.ERROR;
public RunBootstrapCommand(final ConfigurationProvider configurationProvider, final ProcessHandleProvider processHandleProvider) {
this.configurationProvider = Objects.requireNonNull(configurationProvider);
this.processHandleProvider = Objects.requireNonNull(processHandleProvider);
this.managementServerAddressProvider = new StandardManagementServerAddressProvider(configurationProvider);
}
@Override
public CommandStatus getCommandStatus() {
return commandStatus;
}
@Override
public void run() {
try {
final Optional<ProcessHandle> applicationProcessHandle = processHandleProvider.findApplicationProcessHandle();
if (applicationProcessHandle.isEmpty()) {
writePlatformProperties();
runtimeValidatorExecutor.execute();
final ApplicationPropertyHandler securityApplicationPropertyHandler = new SecurityApplicationPropertyHandler(logger);
securityApplicationPropertyHandler.handleProperties(configurationProvider.getApplicationProperties());
final ProcessBuilderProvider processBuilderProvider = new StandardProcessBuilderProvider(configurationProvider, managementServerAddressProvider);
final ProcessBuilder processBuilder = processBuilderProvider.getApplicationProcessBuilder();
processBuilder.inheritIO();
final String command = String.join(SPACE_SEPARATOR, processBuilder.command());
logger.info(command);
final Process process = processBuilder.start();
if (process.isAlive()) {
commandStatus = CommandStatus.SUCCESS;
commandLogger.info("Application Process [{}] started", process.pid());
} else {
commandStatus = CommandStatus.STOPPED;
commandLogger.error("Application Process [{}] start failed", process.pid());
}
} else {
commandLogger.info("Application Process [{}] running", applicationProcessHandle.get().pid());
commandStatus = CommandStatus.ERROR;
}
} catch (final Throwable e) {
commandLogger.warn("Application Process run failed", e);
commandStatus = CommandStatus.FAILED;
}
}
private void writePlatformProperties() {
final Runtime.Version version = Runtime.version();
logger.info("Java Version: {}", version);
final Runtime runtime = Runtime.getRuntime();
logger.info("Available Processors: {}", runtime.availableProcessors());
final OperatingSystemMXBean operatingSystem = ManagementFactory.getOperatingSystemMXBean();
if (operatingSystem instanceof UnixOperatingSystemMXBean unixOperatingSystem) {
logger.info("Total Memory: {}", unixOperatingSystem.getTotalMemorySize());
logger.info("Maximum File Descriptors: {}", unixOperatingSystem.getMaxFileDescriptorCount());
}
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.bootstrap.command;
import java.util.List;
import java.util.Objects;
/**
* Sequence of Bootstrap Commands
*/
class SequenceBootstrapCommand implements BootstrapCommand {
private final List<BootstrapCommand> bootstrapCommands;
private CommandStatus commandStatus = CommandStatus.ERROR;
SequenceBootstrapCommand(final List<BootstrapCommand> bootstrapCommands) {
this.bootstrapCommands = Objects.requireNonNull(bootstrapCommands);
}
@Override
public CommandStatus getCommandStatus() {
return commandStatus;
}
@Override
public void run() {
for (final BootstrapCommand bootstrapCommand : bootstrapCommands) {
bootstrapCommand.run();
commandStatus = bootstrapCommand.getCommandStatus();
if (CommandStatus.SUCCESS != commandStatus) {
break;
}
}
}
}

View File

@ -0,0 +1,238 @@
/*
* 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.bootstrap.command;
import org.apache.nifi.bootstrap.command.io.BootstrapArgument;
import org.apache.nifi.bootstrap.command.io.BootstrapArgumentParser;
import org.apache.nifi.bootstrap.command.io.FileResponseStreamHandler;
import org.apache.nifi.bootstrap.command.io.LoggerResponseStreamHandler;
import org.apache.nifi.bootstrap.command.io.ResponseStreamHandler;
import org.apache.nifi.bootstrap.command.io.StandardBootstrapArgumentParser;
import org.apache.nifi.bootstrap.command.process.StandardProcessHandleProvider;
import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.apache.nifi.bootstrap.configuration.StandardConfigurationProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static java.net.HttpURLConnection.HTTP_ACCEPTED;
import static java.net.HttpURLConnection.HTTP_OK;
import static org.apache.nifi.bootstrap.command.io.HttpRequestMethod.DELETE;
import static org.apache.nifi.bootstrap.command.io.HttpRequestMethod.GET;
import static org.apache.nifi.bootstrap.configuration.ManagementServerPath.HEALTH;
import static org.apache.nifi.bootstrap.configuration.ManagementServerPath.HEALTH_CLUSTER;
import static org.apache.nifi.bootstrap.configuration.ManagementServerPath.HEALTH_DIAGNOSTICS;
import static org.apache.nifi.bootstrap.configuration.ManagementServerPath.HEALTH_STATUS_HISTORY;
/**
* Standard implementation of Bootstrap Command Provider with parsing of supported commands
*/
public class StandardBootstrapCommandProvider implements BootstrapCommandProvider {
private static final String SHUTDOWN_REQUESTED = "--shutdown=true";
private static final String VERBOSE_REQUESTED = "--verbose";
private static final String VERBOSE_QUERY = "verbose=true";
private static final String DAYS_QUERY = "days=%d";
private static final String EMPTY_QUERY = null;
private static final int FIRST_ARGUMENT = 1;
private static final int SECOND_ARGUMENT = 2;
private static final int PATH_ARGUMENTS = 2;
private static final int DAYS_PATH_ARGUMENTS = 3;
private static final int DAYS_REQUESTED_DEFAULT = 1;
private static final BootstrapArgumentParser bootstrapArgumentParser = new StandardBootstrapArgumentParser();
private static final Logger commandLogger = LoggerFactory.getLogger(ApplicationClassName.BOOTSTRAP_COMMAND.getName());
/**
* Get Bootstrap Command
*
* @param arguments Application arguments
* @return Bootstrap Command to run
*/
@Override
public BootstrapCommand getBootstrapCommand(final String[] arguments) {
final BootstrapCommand bootstrapCommand;
final Optional<BootstrapArgument> bootstrapArgumentFound = bootstrapArgumentParser.getBootstrapArgument(arguments);
if (bootstrapArgumentFound.isPresent()) {
final BootstrapArgument bootstrapArgument = bootstrapArgumentFound.get();
bootstrapCommand = getBootstrapCommand(bootstrapArgument, arguments);
} else {
bootstrapCommand = new UnknownBootstrapCommand();
}
return bootstrapCommand;
}
private BootstrapCommand getBootstrapCommand(final BootstrapArgument bootstrapArgument, final String[] arguments) {
final ConfigurationProvider configurationProvider = new StandardConfigurationProvider(System.getenv(), System.getProperties());
final ProcessHandleProvider processHandleProvider = new StandardProcessHandleProvider(configurationProvider);
final ResponseStreamHandler commandLoggerStreamHandler = new LoggerResponseStreamHandler(commandLogger);
final BootstrapCommand stopBootstrapCommand = new StopBootstrapCommand(processHandleProvider, configurationProvider);
final BootstrapCommand bootstrapCommand;
if (BootstrapArgument.CLUSTER_STATUS == bootstrapArgument) {
bootstrapCommand = new ManagementServerBootstrapCommand(processHandleProvider, HEALTH_CLUSTER, commandLoggerStreamHandler);
} else if (BootstrapArgument.DECOMMISSION == bootstrapArgument) {
bootstrapCommand = getDecommissionCommand(processHandleProvider, stopBootstrapCommand, arguments);
} else if (BootstrapArgument.DIAGNOSTICS == bootstrapArgument) {
bootstrapCommand = getDiagnosticsCommand(processHandleProvider, arguments);
} else if (BootstrapArgument.GET_RUN_COMMAND == bootstrapArgument) {
bootstrapCommand = new GetRunCommandBootstrapCommand(configurationProvider, processHandleProvider, System.out);
} else if (BootstrapArgument.START == bootstrapArgument) {
final BootstrapCommand runBootstrapCommand = new RunBootstrapCommand(configurationProvider, processHandleProvider);
final ProcessHandle currentProcessHandle = ProcessHandle.current();
final BootstrapCommand statusBootstrapCommand = new ApplicationProcessStatusBootstrapCommand(currentProcessHandle);
bootstrapCommand = new StartBootstrapCommand(runBootstrapCommand, statusBootstrapCommand);
} else if (BootstrapArgument.STATUS == bootstrapArgument) {
bootstrapCommand = new ManagementServerBootstrapCommand(processHandleProvider, HEALTH, commandLoggerStreamHandler);
} else if (BootstrapArgument.STATUS_HISTORY == bootstrapArgument) {
bootstrapCommand = getStatusHistoryCommand(processHandleProvider, arguments);
} else if (BootstrapArgument.STOP == bootstrapArgument) {
bootstrapCommand = stopBootstrapCommand;
} else {
bootstrapCommand = new UnknownBootstrapCommand();
}
return bootstrapCommand;
}
private BootstrapCommand getDecommissionCommand(final ProcessHandleProvider processHandleProvider, final BootstrapCommand stopBootstrapCommand, final String[] arguments) {
final ResponseStreamHandler responseStreamHandler = new LoggerResponseStreamHandler(commandLogger);
final List<BootstrapCommand> bootstrapCommands = new ArrayList<>();
final BootstrapCommand decommissionCommand = new ManagementServerBootstrapCommand(processHandleProvider, DELETE, HEALTH_CLUSTER, EMPTY_QUERY, HTTP_ACCEPTED, responseStreamHandler);
bootstrapCommands.add(decommissionCommand);
if (isShutdownRequested(arguments)) {
bootstrapCommands.add(stopBootstrapCommand);
}
return new SequenceBootstrapCommand(bootstrapCommands);
}
private BootstrapCommand getDiagnosticsCommand(final ProcessHandleProvider processHandleProvider, final String[] arguments) {
final String verboseQuery = getVerboseQuery(arguments);
final ResponseStreamHandler responseStreamHandler = getDiagnosticsResponseStreamHandler(arguments);
return new ManagementServerBootstrapCommand(processHandleProvider, GET, HEALTH_DIAGNOSTICS, verboseQuery, HTTP_OK, responseStreamHandler);
}
private ResponseStreamHandler getDiagnosticsResponseStreamHandler(final String[] arguments) {
final ResponseStreamHandler responseStreamHandler;
if (arguments.length == PATH_ARGUMENTS) {
final String outputPathArgument = arguments[FIRST_ARGUMENT];
final Path outputPath = Paths.get(outputPathArgument);
responseStreamHandler = new FileResponseStreamHandler(outputPath);
} else {
final Logger logger = LoggerFactory.getLogger(StandardBootstrapCommandProvider.class);
responseStreamHandler = new LoggerResponseStreamHandler(logger);
}
return responseStreamHandler;
}
private BootstrapCommand getStatusHistoryCommand(final ProcessHandleProvider processHandleProvider, final String[] arguments) {
final String daysQuery = getStatusHistoryDaysQuery(arguments);
final ResponseStreamHandler responseStreamHandler = getStatusHistoryResponseStreamHandler(arguments);
return new ManagementServerBootstrapCommand(processHandleProvider, GET, HEALTH_STATUS_HISTORY, daysQuery, HTTP_OK, responseStreamHandler);
}
private boolean isShutdownRequested(final String[] arguments) {
boolean shutdownRequested = false;
for (final String argument : arguments) {
if (SHUTDOWN_REQUESTED.contentEquals(argument)) {
shutdownRequested = true;
break;
}
}
return shutdownRequested;
}
private String getVerboseQuery(final String[] arguments) {
String query = null;
for (final String argument : arguments) {
if (VERBOSE_REQUESTED.contentEquals(argument)) {
query = VERBOSE_QUERY;
break;
}
}
return query;
}
private String getStatusHistoryDaysQuery(final String[] arguments) {
final int daysRequested;
if (arguments.length == DAYS_PATH_ARGUMENTS) {
final String daysRequestArgument = arguments[FIRST_ARGUMENT];
daysRequested = getStatusHistoryDaysRequested(daysRequestArgument);
} else {
daysRequested = DAYS_REQUESTED_DEFAULT;
}
return DAYS_QUERY.formatted(daysRequested);
}
private int getStatusHistoryDaysRequested(final String daysRequestArgument) {
int daysRequested;
try {
daysRequested = Integer.parseInt(daysRequestArgument);
} catch (final NumberFormatException e) {
throw new IllegalArgumentException("Status History Days requested not valid");
}
return daysRequested;
}
private ResponseStreamHandler getStatusHistoryResponseStreamHandler(final String[] arguments) {
final ResponseStreamHandler responseStreamHandler;
if (arguments.length == PATH_ARGUMENTS) {
final String outputPathArgument = arguments[FIRST_ARGUMENT];
final Path outputPath = Paths.get(outputPathArgument);
responseStreamHandler = new FileResponseStreamHandler(outputPath);
} else if (arguments.length == DAYS_PATH_ARGUMENTS) {
final String outputPathArgument = arguments[SECOND_ARGUMENT];
final Path outputPath = Paths.get(outputPathArgument);
responseStreamHandler = new FileResponseStreamHandler(outputPath);
} else {
final Logger logger = LoggerFactory.getLogger(StandardBootstrapCommandProvider.class);
responseStreamHandler = new LoggerResponseStreamHandler(logger);
}
return responseStreamHandler;
}
}

View File

@ -0,0 +1,94 @@
/*
* 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.bootstrap.command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Start Bootstrap Command executes the Run Command and monitors status
*/
class StartBootstrapCommand implements BootstrapCommand {
private static final long MONITOR_INTERVAL = 5;
private static final Logger logger = LoggerFactory.getLogger(StartBootstrapCommand.class);
private final BootstrapCommand runCommand;
private final BootstrapCommand statusCommand;
private final ScheduledExecutorService scheduledExecutorService;
private CommandStatus commandStatus = CommandStatus.ERROR;
StartBootstrapCommand(final BootstrapCommand runCommand, final BootstrapCommand statusCommand) {
this.runCommand = Objects.requireNonNull(runCommand);
this.statusCommand = Objects.requireNonNull(statusCommand);
this.scheduledExecutorService = Executors.newScheduledThreadPool(1, command -> {
final Thread thread = new Thread(command);
thread.setName(StartBootstrapCommand.class.getSimpleName());
return thread;
});
}
@Override
public CommandStatus getCommandStatus() {
return commandStatus;
}
@Override
public void run() {
runCommand.run();
commandStatus = runCommand.getCommandStatus();
if (CommandStatus.SUCCESS == commandStatus) {
logger.info("Application watch started");
final WatchCommand watchCommand = new WatchCommand();
scheduledExecutorService.scheduleAtFixedRate(watchCommand, MONITOR_INTERVAL, MONITOR_INTERVAL, TimeUnit.SECONDS);
commandStatus = CommandStatus.RUNNING;
} else {
scheduledExecutorService.shutdown();
}
}
private class WatchCommand implements Runnable {
@Override
public void run() {
statusCommand.run();
final CommandStatus status = statusCommand.getCommandStatus();
if (CommandStatus.SUCCESS == status) {
logger.debug("Application running");
} else if (CommandStatus.FAILED == status) {
logger.error("Application watch failed");
scheduledExecutorService.shutdown();
logger.info("Application watch stopped");
commandStatus = CommandStatus.FAILED;
} else {
logger.warn("Application not running [{}]", status);
runCommand.run();
}
}
}
}

View File

@ -0,0 +1,142 @@
/*
* 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.bootstrap.command;
import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* Bootstrap Command to run for stopping application
*/
class StopBootstrapCommand implements BootstrapCommand {
private static final Duration FORCE_TERMINATION_TIMEOUT = Duration.ofSeconds(5);
private static final Logger logger = LoggerFactory.getLogger(ApplicationClassName.BOOTSTRAP_COMMAND.getName());
private final ProcessHandleProvider processHandleProvider;
private final ConfigurationProvider configurationProvider;
private CommandStatus commandStatus = CommandStatus.ERROR;
StopBootstrapCommand(final ProcessHandleProvider processHandleProvider, final ConfigurationProvider configurationProvider) {
this.processHandleProvider = Objects.requireNonNull(processHandleProvider);
this.configurationProvider = Objects.requireNonNull(configurationProvider);
}
@Override
public CommandStatus getCommandStatus() {
return commandStatus;
}
@Override
public void run() {
final Optional<ProcessHandle> processHandle = processHandleProvider.findApplicationProcessHandle();
if (processHandle.isEmpty()) {
commandStatus = CommandStatus.SUCCESS;
logger.info("Application Process not running");
} else {
stopBootstrapProcess();
destroy(processHandle.get());
}
}
private void stopBootstrapProcess() {
final Optional<ProcessHandle> bootstrapProcessHandleFound = processHandleProvider.findBootstrapProcessHandle();
if (bootstrapProcessHandleFound.isPresent()) {
final ProcessHandle bootstrapProcessHandle = bootstrapProcessHandleFound.get();
final boolean destroyRequested = bootstrapProcessHandle.destroy();
final long pid = bootstrapProcessHandle.pid();
if (destroyRequested) {
logger.info("Bootstrap Process [{}] termination requested", pid);
onBootstrapDestroyCompleted(bootstrapProcessHandle);
} else {
logger.warn("Bootstrap Process [{}] termination request failed", pid);
}
}
}
private void onBootstrapDestroyCompleted(final ProcessHandle bootstrapProcessHandle) {
final long pid = bootstrapProcessHandle.pid();
final CompletableFuture<ProcessHandle> onExitHandle = bootstrapProcessHandle.onExit();
try {
final ProcessHandle completedProcessHandle = onExitHandle.get(FORCE_TERMINATION_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
logger.info("Bootstrap Process [{}] termination completed", completedProcessHandle.pid());
} catch (final Exception e) {
logger.warn("Bootstrap Process [{}] termination failed", pid);
}
}
private void destroy(final ProcessHandle applicationProcessHandle) {
final boolean destroyRequested = applicationProcessHandle.destroy();
logger.info("Application Process [{}] termination requested", applicationProcessHandle.pid());
if (destroyRequested) {
onDestroyCompleted(applicationProcessHandle);
} else {
logger.warn("Application Process [{}] termination request failed", applicationProcessHandle.pid());
destroyForcibly(applicationProcessHandle);
}
}
private void destroyForcibly(final ProcessHandle applicationProcessHandle) {
final boolean destroyForciblyRequested = applicationProcessHandle.destroyForcibly();
if (destroyForciblyRequested) {
logger.warn("Application Process [{}] force termination failed", applicationProcessHandle.pid());
} else {
onDestroyForciblyCompleted(applicationProcessHandle);
}
}
private void onDestroyCompleted(final ProcessHandle applicationProcessHandle) {
final long pid = applicationProcessHandle.pid();
final CompletableFuture<ProcessHandle> onExitHandle = applicationProcessHandle.onExit();
final Duration gracefulShutdownTimeout = configurationProvider.getGracefulShutdownTimeout();
try {
final ProcessHandle completedProcessHandle = onExitHandle.get(gracefulShutdownTimeout.toSeconds(), TimeUnit.SECONDS);
logger.info("Application Process [{}] termination completed", completedProcessHandle.pid());
commandStatus = CommandStatus.SUCCESS;
} catch (final Exception e) {
logger.warn("Application Process [{}] termination failed", pid);
destroyForcibly(applicationProcessHandle);
}
}
private void onDestroyForciblyCompleted(final ProcessHandle applicationProcessHandle) {
final long pid = applicationProcessHandle.pid();
final CompletableFuture<ProcessHandle> onExitHandle = applicationProcessHandle.onExit();
try {
final ProcessHandle completedProcessHandle = onExitHandle.get(FORCE_TERMINATION_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
logger.warn("Application Process [{}] force termination completed", completedProcessHandle.pid());
commandStatus = CommandStatus.SUCCESS;
} catch (final Exception e) {
logger.warn("Application Process [{}] force termination request failed", pid);
commandStatus = CommandStatus.ERROR;
}
}
}

View File

@ -0,0 +1,33 @@
/*
* 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.bootstrap.command;
/**
* Bootstrap Command to run for an unknown command requested
*/
class UnknownBootstrapCommand implements BootstrapCommand {
@Override
public CommandStatus getCommandStatus() {
return CommandStatus.ERROR;
}
@Override
public void run() {
}
}

View File

@ -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.
*/
package org.apache.nifi.bootstrap.command.io;
/**
* Enumeration of supported arguments for the bootstrap application
*/
public enum BootstrapArgument {
CLUSTER_STATUS,
DECOMMISSION,
DIAGNOSTICS,
GET_RUN_COMMAND,
STATUS,
STATUS_HISTORY,
START,
STOP
}

View File

@ -0,0 +1,32 @@
/*
* 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.bootstrap.command.io;
import java.util.Optional;
/**
* Abstraction for parsing application arguments to Bootstrap Arguments
*/
public interface BootstrapArgumentParser {
/**
* Get Bootstrap Argument from application arguments
*
* @param arguments Application array of arguments
* @return Bootstrap Argument or empty when not found
*/
Optional<BootstrapArgument> getBootstrapArgument(String[] arguments);
}

View File

@ -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.
*/
package org.apache.nifi.bootstrap.command.io;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
/**
* File implementation responsible for reading and transferring responses
*/
public class FileResponseStreamHandler implements ResponseStreamHandler {
private static final Logger logger = LoggerFactory.getLogger(FileResponseStreamHandler.class);
private final Path outputPath;
public FileResponseStreamHandler(final Path outputPath) {
this.outputPath = Objects.requireNonNull(outputPath);
}
@Override
public void onResponseStream(final InputStream responseStream) {
try (OutputStream outputStream = Files.newOutputStream(outputPath)) {
responseStream.transferTo(outputStream);
} catch (final IOException e) {
logger.warn("Write response stream failed for [%s]".formatted(outputPath), e);
}
}
}

View File

@ -14,11 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi;
package org.apache.nifi.bootstrap.command.io;
public interface NiFiEntryPoint {
/**
* Enumeration of supported HTTP Request Methods for Management Server
*/
public enum HttpRequestMethod {
DELETE,
NiFiServer getServer();
void shutdownHook(boolean isReload);
GET
}

View File

@ -0,0 +1,45 @@
/*
* 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.bootstrap.command.io;
import org.slf4j.Logger;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Objects;
/**
* Logger implementation responsible for reading and logging stream of lines
*/
public class LoggerResponseStreamHandler implements ResponseStreamHandler {
private final Logger logger;
public LoggerResponseStreamHandler(final Logger logger) {
this.logger = Objects.requireNonNull(logger);
}
@Override
public void onResponseStream(final InputStream responseStream) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseStream))) {
reader.lines().forEach(logger::info);
} catch (final IOException e) {
logger.warn("Read response stream failed", e);
}
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.bootstrap.command.io;
import java.io.InputStream;
/**
* Response Stream Handler abstraction for reading Management Server responses
*/
public interface ResponseStreamHandler {
/**
* Handle response stream from Management Server
*
* @param responseStream Response Stream
*/
void onResponseStream(InputStream responseStream);
}

View File

@ -0,0 +1,57 @@
/*
* 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.bootstrap.command.io;
import java.util.Arrays;
import java.util.Optional;
/**
* Standard implementation of Bootstrap Argument Parser supporting enumerated arguments as the first element in an array
*/
public class StandardBootstrapArgumentParser implements BootstrapArgumentParser {
private static final char HYPHEN = '-';
private static final char UNDERSCORE = '_';
/**
* Get Bootstrap Argument from first argument provided
*
* @param arguments Application array of arguments
* @return Bootstrap Argument or empty when not found
*/
@Override
public Optional<BootstrapArgument> getBootstrapArgument(final String[] arguments) {
final Optional<BootstrapArgument> bootstrapArgumentFound;
if (arguments == null || arguments.length == 0) {
bootstrapArgumentFound = Optional.empty();
} else {
final String firstArgument = arguments[0];
final String formattedArgument = getFormattedArgument(firstArgument);
bootstrapArgumentFound = Arrays.stream(BootstrapArgument.values())
.filter(bootstrapArgument -> bootstrapArgument.name().equals(formattedArgument))
.findFirst();
}
return bootstrapArgumentFound;
}
private String getFormattedArgument(final String argument) {
final String upperCased = argument.toUpperCase();
return upperCased.replace(HYPHEN, UNDERSCORE);
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.bootstrap.command.process;
import java.util.Optional;
/**
* Abstraction for producing or locating the Management Server socket address
*/
public interface ManagementServerAddressProvider {
/**
* Get Management Server Address with port number
*
* @return Management Server Address or empty when not found
*/
Optional<String> getAddress();
}

View File

@ -0,0 +1,29 @@
/*
* 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.bootstrap.command.process;
/**
* Abstraction for creating a Process Builder
*/
public interface ProcessBuilderProvider {
/**
* Get Application Process Builder
*
* @return Process Builder for Application with command arguments configured
*/
ProcessBuilder getApplicationProcessBuilder();
}

View File

@ -0,0 +1,75 @@
/*
* 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.bootstrap.command.process;
import org.apache.nifi.bootstrap.configuration.SystemProperty;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Provider implementation resolves the Management Server Address from command arguments of the application Process Handle
*/
public class ProcessHandleManagementServerAddressProvider implements ManagementServerAddressProvider {
private static final Pattern ADDRESS_ARGUMENT_PATTERN = Pattern.compile("^-D%s=(.+?)$".formatted(SystemProperty.MANAGEMENT_SERVER_ADDRESS.getProperty()));
private static final int ADDRESS_GROUP = 1;
private final ProcessHandle processHandle;
public ProcessHandleManagementServerAddressProvider(final ProcessHandle processHandle) {
this.processHandle = Objects.requireNonNull(processHandle);
}
/**
* Get Management Server Address with port number from command argument in Process Handle
*
* @return Management Server Address or null when not found
*/
@Override
public Optional<String> getAddress() {
final ProcessHandle.Info info = processHandle.info();
final String managementServerAddress;
final Optional<String[]> argumentsFound = info.arguments();
if (argumentsFound.isPresent()) {
final String[] arguments = argumentsFound.get();
managementServerAddress = findManagementServerAddress(arguments);
} else {
managementServerAddress = null;
}
return Optional.ofNullable(managementServerAddress);
}
private String findManagementServerAddress(final String[] arguments) {
String managementServerAddress = null;
for (final String argument : arguments) {
final Matcher matcher = ADDRESS_ARGUMENT_PATTERN.matcher(argument);
if (matcher.matches()) {
managementServerAddress = matcher.group(ADDRESS_GROUP);
break;
}
}
return managementServerAddress;
}
}

View File

@ -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.
*/
package org.apache.nifi.bootstrap.command.process;
import java.util.Optional;
/**
* Abstraction for finding a Process Handle
*/
public interface ProcessHandleProvider {
/**
* Find Application Process Handle
*
* @return Application Process Handle or empty when not found
*/
Optional<ProcessHandle> findApplicationProcessHandle();
/**
* Find Bootstrap Process Handle
*
* @return Bootstrap Process Handle or empty when not found
*/
Optional<ProcessHandle> findBootstrapProcessHandle();
}

View File

@ -0,0 +1,86 @@
/*
* 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.bootstrap.command.process;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.URI;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.stream.IntStream;
/**
* Standard Provider reads optional Server Address from Configuration Provider or selects from available ports
*/
public class StandardManagementServerAddressProvider implements ManagementServerAddressProvider {
private static final int STANDARD_PORT = 52020;
private static final int MAXIMUM_PORT = 52050;
private static final String NOT_AVAILABLE_MESSAGE = "Management Server Port not available in range [%d-%d]".formatted(STANDARD_PORT, MAXIMUM_PORT);
private static final String LOCALHOST_ADDRESS = "127.0.0.1:%d";
private static final String HOST_ADDRESS = "%s:%d";
private final ConfigurationProvider configurationProvider;
public StandardManagementServerAddressProvider(final ConfigurationProvider configurationProvider) {
this.configurationProvider = Objects.requireNonNull(configurationProvider);
}
/**
* Get Management Server Address with port number
*
* @return Management Server Address
*/
@Override
public Optional<String> getAddress() {
final Optional<String> address;
final Optional<URI> managementServerAddress = configurationProvider.getManagementServerAddress();
if (managementServerAddress.isPresent()) {
final URI serverAddress = managementServerAddress.get();
final String hostAddress = HOST_ADDRESS.formatted(serverAddress.getHost(), serverAddress.getPort());
address = Optional.of(hostAddress);
} else {
final int serverPort = getServerPort();
address = Optional.of(LOCALHOST_ADDRESS.formatted(serverPort));
}
return address;
}
private int getServerPort() {
final OptionalInt portFound = IntStream.range(STANDARD_PORT, MAXIMUM_PORT)
.filter(StandardManagementServerAddressProvider::isPortFree)
.findFirst();
return portFound.orElseThrow(() -> new IllegalStateException(NOT_AVAILABLE_MESSAGE));
}
private static boolean isPortFree(final int port) {
try (ServerSocket ignored = new ServerSocket(port)) {
return true;
} catch (final IOException e) {
return false;
}
}
}

View File

@ -0,0 +1,118 @@
/*
* 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.bootstrap.command.process;
import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.apache.nifi.bootstrap.configuration.SystemProperty;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiPredicate;
import java.util.stream.Stream;
/**
* Standard implementation of Process Builder Provider for constructing application command arguments
*/
public class StandardProcessBuilderProvider implements ProcessBuilderProvider {
private static final String JAR_FILE_EXTENSION = ".jar";
private static final BiPredicate<Path, BasicFileAttributes> JAR_FILE_MATCHER = (path, attributes) -> path.getFileName().toString().endsWith(JAR_FILE_EXTENSION);
private static final int LIBRARY_JAR_DEPTH = 1;
private static final String SYSTEM_PROPERTY = "-D%s=%s";
private static final String CLASS_PATH_ARGUMENT = "--class-path";
private final ConfigurationProvider configurationProvider;
private final ManagementServerAddressProvider managementServerAddressProvider;
public StandardProcessBuilderProvider(final ConfigurationProvider configurationProvider, final ManagementServerAddressProvider managementServerAddressProvider) {
this.configurationProvider = Objects.requireNonNull(configurationProvider);
this.managementServerAddressProvider = Objects.requireNonNull(managementServerAddressProvider);
}
@Override
public ProcessBuilder getApplicationProcessBuilder() {
final ProcessBuilder processBuilder = new ProcessBuilder();
final List<String> command = getCommand();
processBuilder.command(command);
return processBuilder;
}
private List<String> getCommand() {
final List<String> command = new ArrayList<>();
final ProcessHandle.Info currentProcessHandleInfo = ProcessHandle.current().info();
final String currentProcessCommand = getCurrentProcessCommand(currentProcessHandleInfo);
command.add(currentProcessCommand);
final String classPath = getClassPath();
command.add(CLASS_PATH_ARGUMENT);
command.add(classPath);
final Path logDirectory = configurationProvider.getLogDirectory();
final String logDirectoryProperty = SYSTEM_PROPERTY.formatted(SystemProperty.LOG_DIRECTORY.getProperty(), logDirectory);
command.add(logDirectoryProperty);
final Path applicationProperties = configurationProvider.getApplicationProperties();
final String applicationPropertiesProperty = SYSTEM_PROPERTY.formatted(SystemProperty.APPLICATION_PROPERTIES.getProperty(), applicationProperties);
command.add(applicationPropertiesProperty);
final String managementServerAddress = managementServerAddressProvider.getAddress().orElseThrow(() -> new IllegalStateException("Management Server Address not configured"));
final String managementServerAddressProperty = SYSTEM_PROPERTY.formatted(SystemProperty.MANAGEMENT_SERVER_ADDRESS.getProperty(), managementServerAddress);
command.add(managementServerAddressProperty);
final List<String> additionalArguments = configurationProvider.getAdditionalArguments();
command.addAll(additionalArguments);
command.add(ApplicationClassName.APPLICATION.getName());
return command;
}
private String getCurrentProcessCommand(final ProcessHandle.Info currentProcessHandleInfo) {
final Optional<String> currentProcessHandleCommand = currentProcessHandleInfo.command();
return currentProcessHandleCommand.orElseThrow(IllegalStateException::new);
}
private String getClassPath() {
final Path libraryDirectory = configurationProvider.getLibraryDirectory();
try (
Stream<Path> libraryFiles = Files.find(libraryDirectory, LIBRARY_JAR_DEPTH, JAR_FILE_MATCHER)
) {
final List<String> libraryPaths = new ArrayList<>(libraryFiles.map(Path::toString).toList());
final Path configurationDirectory = configurationProvider.getConfigurationDirectory();
libraryPaths.add(configurationDirectory.toString());
return String.join(File.pathSeparator, libraryPaths);
} catch (final IOException e) {
throw new IllegalStateException("Read Library Directory [%s] failed".formatted(libraryDirectory), e);
}
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.bootstrap.command.process;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.apache.nifi.bootstrap.configuration.SystemProperty;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
/**
* Process Handle Provider searches running Processes and locates the first handles match based on command arguments
*/
public class StandardProcessHandleProvider implements ProcessHandleProvider {
private static final String PROPERTIES_ARGUMENT = "-D%s=%s";
private final ConfigurationProvider configurationProvider;
public StandardProcessHandleProvider(final ConfigurationProvider configurationProvider) {
this.configurationProvider = Objects.requireNonNull(configurationProvider);
}
/**
* Find Process Handle for Application based on matching argument for path to application properties
*
* @return Application Process Handle or empty when not found
*/
@Override
public Optional<ProcessHandle> findApplicationProcessHandle() {
final Path applicationProperties = configurationProvider.getApplicationProperties();
return findProcessHandle(SystemProperty.APPLICATION_PROPERTIES, applicationProperties);
}
/**
* Find Process Handle for Bootstrap based on matching argument for path to bootstrap configuration
*
* @return Bootstrap Process Handle or empty when not found
*/
@Override
public Optional<ProcessHandle> findBootstrapProcessHandle() {
final Path bootstrapConfiguration = configurationProvider.getBootstrapConfiguration();
return findProcessHandle(SystemProperty.BOOTSTRAP_CONFIGURATION, bootstrapConfiguration);
}
private Optional<ProcessHandle> findProcessHandle(final SystemProperty systemProperty, final Path configuration) {
final String propertiesArgument = PROPERTIES_ARGUMENT.formatted(systemProperty.getProperty(), configuration);
final ProcessHandle currentProcessHandle = ProcessHandle.current();
return ProcessHandle.allProcesses()
.filter(Predicate.not(currentProcessHandle::equals))
.filter(processHandle -> {
final ProcessHandle.Info processHandleInfo = processHandle.info();
final Optional<String[]> processArguments = processHandleInfo.arguments();
final boolean matched;
if (processArguments.isPresent()) {
final String[] arguments = processArguments.get();
matched = Arrays.stream(arguments).anyMatch(propertiesArgument::contentEquals);
} else {
matched = false;
}
return matched;
})
.findFirst();
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.bootstrap.configuration;
/**
* Enumeration of application class names for processes and logging
*/
public enum ApplicationClassName {
APPLICATION("org.apache.nifi.NiFi"),
BOOTSTRAP_COMMAND("org.apache.nifi.bootstrap.Command");
private final String name;
ApplicationClassName(final String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.bootstrap.configuration;
/**
* Enumeration of supported bootstrap properties for the application
*/
public enum BootstrapProperty {
CONFIGURATION_DIRECTORY("conf.dir"),
GRACEFUL_SHUTDOWN_SECONDS("graceful.shutdown.seconds"),
JAVA_ARGUMENT("java.arg"),
LIBRARY_DIRECTORY("lib.dir"),
MANAGEMENT_SERVER_ADDRESS("management.server.address"),
WORKING_DIRECTORY("working.dir");
private final String property;
BootstrapProperty(final String property) {
this.property = property;
}
public String getProperty() {
return property;
}
}

View File

@ -0,0 +1,91 @@
/*
* 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.bootstrap.configuration;
import java.net.URI;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
/**
* Abstraction for access to application configuration properties
*/
public interface ConfigurationProvider {
/**
* Get additional arguments for application command
*
* @return Additional arguments
*/
List<String> getAdditionalArguments();
/**
* Get file containing application properties
*
* @return Application properties
*/
Path getApplicationProperties();
/**
* Get file containing bootstrap configuration
*
* @return Bootstrap configuration
*/
Path getBootstrapConfiguration();
/**
* Get directory containing application configuration
*
* @return Configuration directory
*/
Path getConfigurationDirectory();
/**
* Get directory containing application libraries
*
* @return Library directory
*/
Path getLibraryDirectory();
/**
* Get directory containing logs
*
* @return Log directory
*/
Path getLogDirectory();
/**
* Get timeout configured for graceful shutdown of application process
*
* @return Graceful Shutdown Timeout duration
*/
Duration getGracefulShutdownTimeout();
/**
* Get Management Server Address from the bootstrap configuration
*
* @return Management Server Address or empty when not configured
*/
Optional<URI> getManagementServerAddress();
/**
* Get directory for current operations and resolving relative paths
*
* @return Working directory
*/
Path getWorkingDirectory();
}

View File

@ -0,0 +1,24 @@
/*
* 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.bootstrap.configuration;
/**
* Enumeration of supported bootstrap and application environment variables
*/
public enum EnvironmentVariable {
NIFI_HOME
}

View File

@ -14,25 +14,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.bootstrap;
package org.apache.nifi.bootstrap.configuration;
public class InvalidCommandException extends Exception {
/**
* Enumeration of Management Server HTTP resource paths
*/
public enum ManagementServerPath {
HEALTH("/health"),
private static final long serialVersionUID = 1L;
HEALTH_CLUSTER("/health/cluster"),
public InvalidCommandException() {
super();
HEALTH_DIAGNOSTICS("/health/diagnostics"),
HEALTH_STATUS_HISTORY("/health/status-history");
private final String path;
ManagementServerPath(final String path) {
this.path = path;
}
public InvalidCommandException(final String message) {
super(message);
}
public InvalidCommandException(final Throwable t) {
super(t);
}
public InvalidCommandException(final String message, final Throwable t) {
super(message, t);
public String getPath() {
return path;
}
}

View File

@ -0,0 +1,290 @@
/*
* 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.bootstrap.configuration;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
/**
* Standard implementation of Configuration Provider based on NIFI_HOME environment variable base directory
*/
public class StandardConfigurationProvider implements ConfigurationProvider {
private static final String CONFIGURATION_DIRECTORY = "conf";
private static final String LIBRARY_DIRECTORY = "lib";
private static final String LOG_DIRECTORY = "logs";
private static final String APPLICATION_PROPERTIES = "nifi.properties";
private static final String BOOTSTRAP_CONFIGURATION = "bootstrap.conf";
private static final String CURRENT_DIRECTORY = "";
private static final Duration GRACEFUL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(20);
private final Map<String, String> environmentVariables;
private final Properties systemProperties;
private final Properties bootstrapProperties = new Properties();
public StandardConfigurationProvider(final Map<String, String> environmentVariables, final Properties systemProperties) {
this.environmentVariables = Objects.requireNonNull(environmentVariables);
this.systemProperties = Objects.requireNonNull(systemProperties);
setBootstrapProperties();
}
/**
* Get additional arguments for application command from Bootstrap Properties starting with java.arg
*
* @return Additional arguments
*/
@Override
public List<String> getAdditionalArguments() {
final List<String> additionalArguments = new ArrayList<>();
for (final String propertyName : bootstrapProperties.stringPropertyNames()) {
if (propertyName.startsWith(BootstrapProperty.JAVA_ARGUMENT.getProperty())) {
final String additionalArgument = bootstrapProperties.getProperty(propertyName);
if (!additionalArgument.isBlank()) {
additionalArguments.add(additionalArgument);
}
}
}
return additionalArguments;
}
/**
* Get Application Properties relative to configuration directory
*
* @return Application Properties
*/
@Override
public Path getApplicationProperties() {
final Path configurationDirectory = getConfigurationDirectory();
final Path applicationProperties = configurationDirectory.resolve(APPLICATION_PROPERTIES);
if (Files.notExists(applicationProperties)) {
throw new IllegalStateException("Application Properties [%s] not found".formatted(applicationProperties));
}
return applicationProperties;
}
/**
* Get Bootstrap Configuration from either System Property or relative to configuration directory
*
* @return Bootstrap Configuration
*/
@Override
public Path getBootstrapConfiguration() {
final Path bootstrapConfiguration;
final String bootstrapConfigurationProperty = System.getProperty(SystemProperty.BOOTSTRAP_CONFIGURATION.getProperty());
if (isEmpty(bootstrapConfigurationProperty)) {
final Path configurationDirectory = getConfigurationDirectory();
bootstrapConfiguration = configurationDirectory.resolve(BOOTSTRAP_CONFIGURATION);
} else {
bootstrapConfiguration = Paths.get(bootstrapConfigurationProperty).toAbsolutePath();
}
if (Files.notExists(bootstrapConfiguration)) {
throw new IllegalStateException("Bootstrap Configuration [%s] not found".formatted(bootstrapConfiguration));
}
return bootstrapConfiguration;
}
/**
* Get Library Directory from Bootstrap Configuration or relative to configuration directory
*
* @return Library Directory
*/
@Override
public Path getLibraryDirectory() {
final Path libraryDirectory = getResolvedDirectory(BootstrapProperty.LIBRARY_DIRECTORY, LIBRARY_DIRECTORY);
if (Files.notExists(libraryDirectory)) {
throw new IllegalStateException("Library Directory [%s] not found".formatted(libraryDirectory));
}
return libraryDirectory;
}
/**
* Get Log Directory from System Property or relative to application home directory
*
* @return Log Directory
*/
@Override
public Path getLogDirectory() {
final Path logDirectory;
final String logDirectoryProperty = systemProperties.getProperty(SystemProperty.LOG_DIRECTORY.getProperty());
if (isEmpty(logDirectoryProperty)) {
final Path applicationHome = getApplicationHome();
logDirectory = applicationHome.resolve(LOG_DIRECTORY);
} else {
logDirectory = Paths.get(logDirectoryProperty);
}
return logDirectory;
}
/**
* Get timeout configured for graceful shutdown of application process
*
* @return Graceful Shutdown Timeout duration
*/
@Override
public Duration getGracefulShutdownTimeout() {
final Duration gracefulShutdownTimeout;
final String gracefulShutdownSecondsProperty = bootstrapProperties.getProperty(BootstrapProperty.GRACEFUL_SHUTDOWN_SECONDS.getProperty());
if (gracefulShutdownSecondsProperty == null || gracefulShutdownSecondsProperty.isEmpty()) {
gracefulShutdownTimeout = GRACEFUL_SHUTDOWN_TIMEOUT;
} else {
final int gracefulShutdownSeconds = Integer.parseInt(gracefulShutdownSecondsProperty);
gracefulShutdownTimeout = Duration.ofSeconds(gracefulShutdownSeconds);
}
return gracefulShutdownTimeout;
}
/**
* Get Management Server Address from the bootstrap configuration
*
* @return Management Server Address or empty when not configured
*/
@Override
public Optional<URI> getManagementServerAddress() {
final Optional<URI> managementServerAddress;
final String managementServerAddressProperty = bootstrapProperties.getProperty(BootstrapProperty.MANAGEMENT_SERVER_ADDRESS.getProperty());
if (managementServerAddressProperty == null || managementServerAddressProperty.isEmpty()) {
managementServerAddress = Optional.empty();
} else {
final URI serverAddress = URI.create(managementServerAddressProperty);
managementServerAddress = Optional.of(serverAddress);
}
return managementServerAddress;
}
/**
* Get Configuration Directory from Bootstrap Configuration or relative to application home directory
*
* @return Configuration Directory
*/
@Override
public Path getConfigurationDirectory() {
final Path configurationDirectory = getResolvedDirectory(BootstrapProperty.CONFIGURATION_DIRECTORY, CONFIGURATION_DIRECTORY);
if (Files.notExists(configurationDirectory)) {
throw new IllegalStateException("Configuration Directory [%s] not found".formatted(configurationDirectory));
}
return configurationDirectory;
}
/**
* Get Working Directory from Bootstrap Configuration or current working directory
*
* @return Working Directory
*/
@Override
public Path getWorkingDirectory() {
final Path workingDirectory;
final String workingDirectoryProperty = bootstrapProperties.getProperty(BootstrapProperty.WORKING_DIRECTORY.getProperty());
if (isEmpty(workingDirectoryProperty)) {
workingDirectory = Paths.get(CURRENT_DIRECTORY).toAbsolutePath();
} else {
workingDirectory = Paths.get(workingDirectoryProperty).toAbsolutePath();
}
return workingDirectory;
}
private Path getResolvedDirectory(final BootstrapProperty bootstrapProperty, final String relativeDirectory) {
final Path resolvedDirectory;
final String directoryProperty = bootstrapProperties.getProperty(bootstrapProperty.getProperty());
if (isEmpty(directoryProperty)) {
final Path applicationHome = getApplicationHome();
resolvedDirectory = applicationHome.resolve(relativeDirectory);
} else {
final Path directoryPropertyResolved = Paths.get(directoryProperty);
if (directoryPropertyResolved.isAbsolute()) {
resolvedDirectory = directoryPropertyResolved;
} else {
final Path workingDirectory = getWorkingDirectory();
resolvedDirectory = workingDirectory.resolve(directoryPropertyResolved);
}
}
// Normalize Path removing relative directory elements
return resolvedDirectory.normalize();
}
private Path getApplicationHome() {
final Path applicationHome;
final String applicationHomeVariable = environmentVariables.get(EnvironmentVariable.NIFI_HOME.name());
if (isEmpty(applicationHomeVariable)) {
throw new IllegalStateException("Application Home Environment Variable [NIFI_HOME] not configured");
} else {
applicationHome = Paths.get(applicationHomeVariable).toAbsolutePath();
}
if (Files.notExists(applicationHome)) {
throw new IllegalStateException("Application Home [%s] not found".formatted(applicationHome));
}
return applicationHome;
}
private boolean isEmpty(final String property) {
return property == null || property.isEmpty();
}
private void setBootstrapProperties() {
final Path bootstrapConfiguration = getBootstrapConfiguration();
try (InputStream inputStream = Files.newInputStream(bootstrapConfiguration)) {
bootstrapProperties.load(inputStream);
} catch (final IOException e) {
throw new UncheckedIOException("Bootstrap Properties [%s] loading failed".formatted(bootstrapConfiguration), e);
}
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.bootstrap.configuration;
/**
* Enumeration of supported system properties for the application
*/
public enum SystemProperty {
/** Path to application properties file */
APPLICATION_PROPERTIES("nifi.properties.file.path"),
/** Path to bootstrap configuration file */
BOOTSTRAP_CONFIGURATION("org.apache.nifi.bootstrap.config.file"),
/** Path to log directory */
LOG_DIRECTORY("org.apache.nifi.bootstrap.config.log.dir"),
/** Socket address and port number for management server */
MANAGEMENT_SERVER_ADDRESS("org.apache.nifi.management.server.address");
private final String property;
SystemProperty(final String property) {
this.property = property;
}
public String getProperty() {
return property;
}
}

View File

@ -1,59 +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.
*/
package org.apache.nifi.bootstrap.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
public final class DumpFileValidator {
private static final Logger logger = LoggerFactory.getLogger(DumpFileValidator.class);
private DumpFileValidator() {
}
public static boolean validate(final String filePath) {
try {
final Path path = Paths.get(filePath);
return checkFileCanBeCreated(path);
} catch (InvalidPathException e) {
System.err.println("Invalid filename. The command parameters are: status-history <number of days> <dumpFile>");
return false;
}
}
private static boolean checkFileCanBeCreated(final Path path) {
try (final FileOutputStream outputStream = new FileOutputStream(path.toString());
final Closeable onClose = () -> Files.delete(path)) {
} catch (FileNotFoundException e) {
System.err.println("Invalid filename or there's no write permission to the currently selected file path.");
return false;
} catch (IOException e) {
logger.error("Could not delete file while validating file path.");
}
return true;
}
}

View File

@ -1,107 +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.
*/
package org.apache.nifi.bootstrap.util;
import java.io.IOException;
import java.io.InputStream;
public class LimitingInputStream extends InputStream {
private final InputStream in;
private final long limit;
private long bytesRead = 0;
public LimitingInputStream(final InputStream in, final long limit) {
this.in = in;
this.limit = limit;
}
@Override
public int read() throws IOException {
if (bytesRead >= limit) {
return -1;
}
final int val = in.read();
if (val > -1) {
bytesRead++;
}
return val;
}
@Override
public int read(final byte[] b) throws IOException {
if (bytesRead >= limit) {
return -1;
}
final int maxToRead = (int) Math.min(b.length, limit - bytesRead);
final int val = in.read(b, 0, maxToRead);
if (val > 0) {
bytesRead += val;
}
return val;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (bytesRead >= limit) {
return -1;
}
final int maxToRead = (int) Math.min(len, limit - bytesRead);
final int val = in.read(b, off, maxToRead);
if (val > 0) {
bytesRead += val;
}
return val;
}
@Override
public long skip(final long n) throws IOException {
final long skipped = in.skip(Math.min(n, limit - bytesRead));
bytesRead += skipped;
return skipped;
}
@Override
public int available() throws IOException {
return in.available();
}
@Override
public void close() throws IOException {
in.close();
}
@Override
public void mark(int readlimit) {
in.mark(readlimit);
}
@Override
public boolean markSupported() {
return in.markSupported();
}
@Override
public void reset() throws IOException {
in.reset();
}
}

View File

@ -0,0 +1,76 @@
/*
* 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.bootstrap.command;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ApplicationProcessStatusBootstrapCommandTest {
@Mock
private ProcessHandle processHandle;
@Mock
private ProcessHandle applicationProcessHandle;
private ApplicationProcessStatusBootstrapCommand command;
@BeforeEach
void setCommand() {
command = new ApplicationProcessStatusBootstrapCommand(processHandle);
}
@Test
void testRunStopped() {
when(processHandle.children()).thenReturn(Stream.empty());
command.run();
final CommandStatus commandStatus = command.getCommandStatus();
assertEquals(CommandStatus.STOPPED, commandStatus);
}
@Test
void testRunSuccess() {
when(processHandle.children()).thenReturn(Stream.of(applicationProcessHandle));
when(applicationProcessHandle.isAlive()).thenReturn(true);
command.run();
final CommandStatus commandStatus = command.getCommandStatus();
assertEquals(CommandStatus.SUCCESS, commandStatus);
}
@Test
void testRunCommunicationFailed() {
when(processHandle.children()).thenReturn(Stream.of(applicationProcessHandle));
when(applicationProcessHandle.isAlive()).thenReturn(false);
command.run();
final CommandStatus commandStatus = command.getCommandStatus();
assertEquals(CommandStatus.COMMUNICATION_FAILED, commandStatus);
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.bootstrap.command;
import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class GetRunCommandBootstrapCommandTest {
private static final String CONFIGURATION_DIRECTORY = "conf";
private static final String SPACE_SEPARATOR = " ";
@Mock
private ConfigurationProvider configurationProvider;
@Mock
private ProcessHandleProvider processHandleProvider;
@Test
void testRun(@TempDir final Path workingDirectory) throws IOException {
final Path configurationDirectory = workingDirectory.resolve(CONFIGURATION_DIRECTORY);
assertTrue(configurationDirectory.toFile().mkdir());
final Path applicationProperties = configurationDirectory.resolve(Properties.class.getSimpleName());
Files.writeString(applicationProperties, SPACE_SEPARATOR);
when(configurationProvider.getApplicationProperties()).thenReturn(applicationProperties);
when(configurationProvider.getLibraryDirectory()).thenReturn(workingDirectory);
when(configurationProvider.getConfigurationDirectory()).thenReturn(configurationDirectory);
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final PrintStream printStream = new PrintStream(outputStream);
final GetRunCommandBootstrapCommand command = new GetRunCommandBootstrapCommand(configurationProvider, processHandleProvider, printStream);
command.run();
final CommandStatus commandStatus = command.getCommandStatus();
assertNotNull(commandStatus);
assertEquals(CommandStatus.SUCCESS, commandStatus);
final String runCommand = outputStream.toString().trim();
final List<String> runCommands = List.of(runCommand.split(SPACE_SEPARATOR));
final String lastCommand = runCommands.getLast();
assertEquals(ApplicationClassName.APPLICATION.getName(), lastCommand);
}
}

View File

@ -0,0 +1,54 @@
/*
* 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.bootstrap.command;
import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(MockitoExtension.class)
class RunBootstrapCommandTest {
@Mock
private ConfigurationProvider configurationProvider;
@Mock
private ProcessHandleProvider processHandleProvider;
private RunBootstrapCommand command;
@BeforeEach
void setCommand() {
command = new RunBootstrapCommand(configurationProvider, processHandleProvider);
}
@Test
void testRun() {
command.run();
final CommandStatus commandStatus = command.getCommandStatus();
assertNotNull(commandStatus);
assertEquals(CommandStatus.FAILED, commandStatus);
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.bootstrap.command;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class StandardBootstrapCommandProviderTest {
private StandardBootstrapCommandProvider provider;
@BeforeEach
void setProvider() {
provider = new StandardBootstrapCommandProvider();
}
@Test
void testGetBootstrapCommandNull() {
final BootstrapCommand bootstrapCommand = provider.getBootstrapCommand(null);
assertNotNull(bootstrapCommand);
assertInstanceOf(UnknownBootstrapCommand.class, bootstrapCommand);
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.bootstrap.command;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class StartBootstrapCommandTest {
@Mock
private BootstrapCommand runBootstrapCommand;
@Mock
private BootstrapCommand statusBootstrapCommand;
private StartBootstrapCommand command;
@BeforeEach
void setCommand() {
command = new StartBootstrapCommand(runBootstrapCommand, statusBootstrapCommand);
}
@Test
void testRunError() {
final CommandStatus runCommandStatus = CommandStatus.ERROR;
when(runBootstrapCommand.getCommandStatus()).thenReturn(runCommandStatus);
command.run();
final CommandStatus commandStatus = command.getCommandStatus();
assertEquals(runCommandStatus, commandStatus);
}
@Test
void testRunSuccessFailed() {
final CommandStatus runCommandStatus = CommandStatus.SUCCESS;
when(runBootstrapCommand.getCommandStatus()).thenReturn(runCommandStatus);
final CommandStatus statusCommandStatus = CommandStatus.FAILED;
lenient().when(statusBootstrapCommand.getCommandStatus()).thenReturn(statusCommandStatus);
command.run();
final CommandStatus commandStatus = command.getCommandStatus();
assertEquals(CommandStatus.RUNNING, commandStatus);
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.bootstrap.command;
import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class StopBootstrapCommandTest {
@Mock
private ConfigurationProvider configurationProvider;
@Mock
private ProcessHandleProvider processHandleProvider;
@Mock
private ProcessHandle applicationProcessHandle;
@Mock
private ProcessHandle.Info applicationProcessHandleInfo;
private StopBootstrapCommand command;
@BeforeEach
void setCommand() {
command = new StopBootstrapCommand(processHandleProvider, configurationProvider);
}
@Test
void testRunProcessHandleNotFound() {
when(processHandleProvider.findApplicationProcessHandle()).thenReturn(Optional.empty());
command.run();
final CommandStatus commandStatus = command.getCommandStatus();
assertEquals(CommandStatus.SUCCESS, commandStatus);
}
@Test
void testRunDestroyCompleted() {
when(processHandleProvider.findApplicationProcessHandle()).thenReturn(Optional.of(applicationProcessHandle));
when(applicationProcessHandle.destroy()).thenReturn(true);
when(applicationProcessHandle.onExit()).thenReturn(CompletableFuture.completedFuture(applicationProcessHandle));
command.run();
final CommandStatus commandStatus = command.getCommandStatus();
assertEquals(CommandStatus.SUCCESS, commandStatus);
}
@Test
void testRunDestroyFailed() {
when(processHandleProvider.findApplicationProcessHandle()).thenReturn(Optional.of(applicationProcessHandle));
when(applicationProcessHandle.destroy()).thenReturn(true);
when(applicationProcessHandle.onExit()).thenReturn(CompletableFuture.failedFuture(new RuntimeException()));
command.run();
final CommandStatus commandStatus = command.getCommandStatus();
assertEquals(CommandStatus.ERROR, commandStatus);
}
}

View File

@ -0,0 +1,47 @@
/*
* 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.bootstrap.command.io;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class FileResponseStreamHandlerTest {
@Test
void testOnResponseStream(@TempDir final Path outputDirectory) throws IOException {
final Path outputPath = outputDirectory.resolve(FileResponseStreamHandlerTest.class.getSimpleName());
final FileResponseStreamHandler handler = new FileResponseStreamHandler(outputPath);
final byte[] bytes = String.class.getName().getBytes(StandardCharsets.UTF_8);
final InputStream inputStream = new ByteArrayInputStream(bytes);
handler.onResponseStream(inputStream);
final byte[] read = Files.readAllBytes(outputPath);
assertArrayEquals(bytes, read);
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.bootstrap.command.io;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class StandardBootstrapArgumentParserTest {
private static final String CLUSTER_STATUS_ARGUMENT = "cluster-status";
private StandardBootstrapArgumentParser parser;
@BeforeEach
void setParser() {
parser = new StandardBootstrapArgumentParser();
}
@Test
void testGetBootstrapArgumentNull() {
final Optional<BootstrapArgument> bootstrapArgumentFound = parser.getBootstrapArgument(null);
assertTrue(bootstrapArgumentFound.isEmpty());
}
@Test
void testGetBootstrapArgumentClusterStatus() {
final Optional<BootstrapArgument> bootstrapArgumentFound = parser.getBootstrapArgument(new String[]{CLUSTER_STATUS_ARGUMENT});
assertTrue(bootstrapArgumentFound.isPresent());
assertEquals(BootstrapArgument.CLUSTER_STATUS, bootstrapArgumentFound.get());
}
}

View File

@ -0,0 +1,86 @@
/*
* 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.bootstrap.command.process;
import org.apache.nifi.bootstrap.configuration.SystemProperty;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ProcessHandleManagementServerAddressProviderTest {
private static final String SYSTEM_PROPERTY_ARGUMENT = "-D%s=%s";
private static final String ADDRESS = "127.0.0.1:52020";
@Mock
private ProcessHandle processHandle;
@Mock
private ProcessHandle.Info processHandleInfo;
private ProcessHandleManagementServerAddressProvider provider;
@BeforeEach
void setProvider() {
provider = new ProcessHandleManagementServerAddressProvider(processHandle);
}
@Test
void testGetAddressNotFound() {
when(processHandle.info()).thenReturn(processHandleInfo);
final Optional<String> managementServerAddress = provider.getAddress();
assertTrue(managementServerAddress.isEmpty());
}
@Test
void testGetAddressArgumentNotFound() {
when(processHandle.info()).thenReturn(processHandleInfo);
when(processHandleInfo.arguments()).thenReturn(Optional.empty());
final Optional<String> managementServerAddress = provider.getAddress();
assertTrue(managementServerAddress.isEmpty());
}
@Test
void testGetAddress() {
when(processHandle.info()).thenReturn(processHandleInfo);
final String systemPropertyArgument = SYSTEM_PROPERTY_ARGUMENT.formatted(SystemProperty.MANAGEMENT_SERVER_ADDRESS.getProperty(), ADDRESS);
final String[] arguments = new String[]{systemPropertyArgument};
when(processHandleInfo.arguments()).thenReturn(Optional.of(arguments));
final Optional<String> managementServerAddress = provider.getAddress();
assertTrue(managementServerAddress.isPresent());
final String address = managementServerAddress.get();
assertEquals(ADDRESS, address);
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.bootstrap.command.process;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(MockitoExtension.class)
class StandardManagementServerAddressProviderTest {
@Mock
private ConfigurationProvider configurationProvider;
private StandardManagementServerAddressProvider provider;
@BeforeEach
void setProvider() {
provider = new StandardManagementServerAddressProvider(configurationProvider);
}
@Test
void testGetAddressFound() {
final Optional<String> managementServerAddress = provider.getAddress();
assertTrue(managementServerAddress.isPresent());
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.bootstrap.command.process;
import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.apache.nifi.bootstrap.configuration.SystemProperty;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class StandardProcessBuilderProviderTest {
private static final String SERVER_ADDRESS = "127.0.0.1:52020";
private static final String SERVER_ADDRESS_PROPERTY = "-D%s=%s".formatted(SystemProperty.MANAGEMENT_SERVER_ADDRESS.getProperty(), SERVER_ADDRESS);
@Mock
private ConfigurationProvider configurationProvider;
@Mock
private ManagementServerAddressProvider managementServerAddressProvider;
private StandardProcessBuilderProvider provider;
@BeforeEach
void setProvider() {
provider = new StandardProcessBuilderProvider(configurationProvider, managementServerAddressProvider);
}
@Test
void testGetApplicationProcessBuilder(@TempDir final Path workingDirectory) {
when(configurationProvider.getLibraryDirectory()).thenReturn(workingDirectory);
when(configurationProvider.getConfigurationDirectory()).thenReturn(workingDirectory);
when(managementServerAddressProvider.getAddress()).thenReturn(Optional.of(SERVER_ADDRESS));
final ProcessBuilder processBuilder = provider.getApplicationProcessBuilder();
assertNotNull(processBuilder);
final List<String> command = processBuilder.command();
final String currentCommand = ProcessHandle.current().info().command().orElse(null);
final String firstCommand = command.getFirst();
assertEquals(currentCommand, firstCommand);
final String lastCommand = command.getLast();
assertEquals(ApplicationClassName.APPLICATION.getName(), lastCommand);
assertTrue(command.contains(SERVER_ADDRESS_PROPERTY));
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.bootstrap.command.process;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(MockitoExtension.class)
class StandardProcessHandleProviderTest {
@Mock
private ConfigurationProvider configurationProvider;
private StandardProcessHandleProvider provider;
@BeforeEach
void setProvider() {
provider = new StandardProcessHandleProvider(configurationProvider);
}
@Test
void testFindApplicationProcessHandleEmpty() {
final Optional<ProcessHandle> applicationProcessHandle = provider.findApplicationProcessHandle();
assertTrue(applicationProcessHandle.isEmpty());
}
@Test
void testFindBootstrapProcessHandleEmpty() {
final Optional<ProcessHandle> bootstrapProcessHandle = provider.findBootstrapProcessHandle();
assertTrue(bootstrapProcessHandle.isEmpty());
}
}

View File

@ -0,0 +1,139 @@
/*
* 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.bootstrap.configuration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class StandardConfigurationProviderTest {
private static final String CONFIGURATION_DIRECTORY = "conf";
private static final String BOOTSTRAP_CONFIGURATION = "bootstrap.conf";
private static final String CURRENT_DIRECTORY = "";
private static final String MANAGEMENT_SERVER_ADDRESS = "http://127.0.0.1:52020";
private final Map<String, String> environmentVariables = new LinkedHashMap<>();
private final Properties systemProperties = new Properties();
@BeforeEach
void setProvider() {
environmentVariables.clear();
systemProperties.clear();
}
@Test
void testApplicationHomeNotConfigured() {
assertThrows(IllegalStateException.class, () -> new StandardConfigurationProvider(environmentVariables, systemProperties));
}
@Test
void testGetBootstrapConfiguration(@TempDir final Path applicationHomeDirectory) throws IOException {
environmentVariables.put(EnvironmentVariable.NIFI_HOME.name(), applicationHomeDirectory.toString());
final Path configurationDirectory = createConfigurationDirectory(applicationHomeDirectory);
final Path bootstrapConfiguration = setRequiredConfiguration(applicationHomeDirectory);
final StandardConfigurationProvider provider = new StandardConfigurationProvider(environmentVariables, systemProperties);
final Path configurationDirectoryProvided = provider.getConfigurationDirectory();
assertEquals(configurationDirectory, configurationDirectoryProvided);
final Path bootstrapConfigurationProvided = provider.getBootstrapConfiguration();
assertEquals(bootstrapConfiguration, bootstrapConfigurationProvided);
}
@Test
void testGetWorkingDirectory(@TempDir final Path applicationHomeDirectory) throws IOException {
setRequiredConfiguration(applicationHomeDirectory);
final StandardConfigurationProvider provider = new StandardConfigurationProvider(environmentVariables, systemProperties);
final Path workingDirectory = provider.getWorkingDirectory();
final Path workingDirectoryExpected = Paths.get(CURRENT_DIRECTORY).toAbsolutePath();
assertEquals(workingDirectoryExpected, workingDirectory);
}
@Test
void testGetManagementServerAddressNotConfigured(@TempDir final Path applicationHomeDirectory) throws IOException {
setRequiredConfiguration(applicationHomeDirectory);
final StandardConfigurationProvider provider = new StandardConfigurationProvider(environmentVariables, systemProperties);
final Optional<URI> managementServerAddress = provider.getManagementServerAddress();
assertTrue(managementServerAddress.isEmpty());
}
@Test
void testGetManagementServerAddress(@TempDir final Path applicationHomeDirectory) throws IOException {
final Path bootstrapConfiguration = setRequiredConfiguration(applicationHomeDirectory);
final Properties bootstrapProperties = new Properties();
bootstrapProperties.put(BootstrapProperty.MANAGEMENT_SERVER_ADDRESS.getProperty(), MANAGEMENT_SERVER_ADDRESS);
try (OutputStream outputStream = Files.newOutputStream(bootstrapConfiguration)) {
bootstrapProperties.store(outputStream, Properties.class.getSimpleName());
}
final StandardConfigurationProvider provider = new StandardConfigurationProvider(environmentVariables, systemProperties);
final Optional<URI> managementServerAddress = provider.getManagementServerAddress();
assertTrue(managementServerAddress.isPresent());
final URI address = managementServerAddress.get();
assertEquals(MANAGEMENT_SERVER_ADDRESS, address.toString());
}
private Path setRequiredConfiguration(final Path applicationHomeDirectory) throws IOException {
environmentVariables.put(EnvironmentVariable.NIFI_HOME.name(), applicationHomeDirectory.toString());
final Path configurationDirectory = createConfigurationDirectory(applicationHomeDirectory);
return createBootstrapConfiguration(configurationDirectory);
}
private Path createConfigurationDirectory(final Path applicationHomeDirectory) {
final Path configurationDirectory = applicationHomeDirectory.resolve(CONFIGURATION_DIRECTORY);
if (configurationDirectory.toFile().mkdir()) {
assertTrue(Files.isReadable(configurationDirectory));
}
return configurationDirectory;
}
private Path createBootstrapConfiguration(final Path configurationDirectory) throws IOException {
final Path bootstrapConfiguration = configurationDirectory.resolve(BOOTSTRAP_CONFIGURATION);
assertTrue(bootstrapConfiguration.toFile().createNewFile());
return bootstrapConfiguration;
}
}

View File

@ -2842,7 +2842,6 @@ properties for minimum and maximum Java Heap size, the garbage collector to use,
|`nifi.diagnostics.on.shutdown.directory`|This property specifies the location of the NiFi diagnostics directory. The default value is `./diagnostics`.
|`nifi.diagnostics.on.shutdown.max.filecount`|This property specifies the maximum permitted number of diagnostic files. If the limit is exceeded, the oldest files are deleted. The default value is `10`.
|`nifi.diagnostics.on.shutdown.max.directory.size`|This property specifies the maximum permitted size of the diagnostics directory. If the limit is exceeded, the oldest files are deleted. The default value is `10 MB`.
|`nifi.bootstrap.listen.port`|This property defines the port used to listen for communications from NiFi. If this property is missing, empty, or `0`, a random ephemeral port is used.
|====
[[proxy_configuration]]

View File

@ -29,17 +29,16 @@ set BOOTSTRAP_LIB_DIR=%NIFI_HOME%\lib\bootstrap
set CONF_DIR=%NIFI_HOME%\conf
set LOG_DIR_PROPERTY=-Dorg.apache.nifi.bootstrap.config.log.dir=%NIFI_LOG_DIR%
set PID_DIR_PROPERTY=-Dorg.apache.nifi.bootstrap.config.pid.dir=%NIFI_PID_DIR%
set CONFIG_FILE_PROPERTY=-Dorg.apache.nifi.bootstrap.config.file=%CONF_DIR%\bootstrap.conf
set PROPERTIES_FILE_PROPERTY=-Dnifi.properties.file.path=%CONF_DIR%\nifi.properties
set BOOTSTRAP_HEAP_SIZE=48m
set JAVA_ARGS=%LOG_DIR_PROPERTY% %PID_DIR_PROPERTY% %CONFIG_FILE_PROPERTY% %PROPERTIES_FILE_PROPERTY%
set JAVA_ARGS=%LOG_DIR_PROPERTY% %CONFIG_FILE_PROPERTY% %PROPERTIES_FILE_PROPERTY%
set JAVA_PARAMS=-cp %BOOTSTRAP_LIB_DIR%\*;%CONF_DIR% %JAVA_ARGS%
set JAVA_MEMORY=-Xms%BOOTSTRAP_HEAP_SIZE% -Xmx%BOOTSTRAP_HEAP_SIZE%
echo JAVA_HOME: %JAVA_HOME%
echo NIFI_HOME: %NIFI_HOME%
echo JAVA_HOME=%JAVA_HOME%
echo NIFI_HOME=%NIFI_HOME%
echo.
pushd %NIFI_HOME%
@ -54,7 +53,7 @@ if %RUN_COMMAND% == "set-single-user-credentials" (
) else if %RUN_COMMAND% == "set-sensitive-properties-algorithm" (
call "%JAVA_EXE%" %JAVA_PARAMS% org.apache.nifi.flow.encryptor.command.SetSensitivePropertiesAlgorithm %~2
) else (
call "%JAVA_EXE%" %JAVA_MEMORY% %JAVA_PARAMS% org.apache.nifi.bootstrap.RunNiFi %RUN_COMMAND%
call "%JAVA_EXE%" %JAVA_MEMORY% %JAVA_PARAMS% org.apache.nifi.bootstrap.BootstrapProcess %RUN_COMMAND%
)
popd

View File

@ -143,15 +143,6 @@ locateJava() {
fi
fi
fi
# if command is env, attempt to add more to the classpath
if [ "$1" = "env" ]; then
[ "x${TOOLS_JAR}" = "x" ] && [ -n "${JAVA_HOME}" ] && TOOLS_JAR=$(find -H "${JAVA_HOME}" -name "tools.jar")
[ "x${TOOLS_JAR}" = "x" ] && [ -n "${JAVA_HOME}" ] && TOOLS_JAR=$(find -H "${JAVA_HOME}" -name "classes.jar")
if [ "x${TOOLS_JAR}" = "x" ]; then
warn "Could not locate tools.jar or classes.jar. Please set manually to avail all command features."
fi
fi
}
init() {
@ -162,7 +153,7 @@ init() {
unlimitFD
# Locate the Java VM to execute
locateJava "$1"
locateJava
}
is_nonzero_integer() {
@ -197,7 +188,6 @@ run() {
NIFI_HOME=$(cygpath --path --windows "${NIFI_HOME}")
NIFI_LOG_DIR=$(cygpath --path --windows "${NIFI_LOG_DIR}")
NIFI_PID_DIR=$(cygpath --path --windows "${NIFI_PID_DIR}")
BOOTSTRAP_CONF=$(cygpath --path --windows "${BOOTSTRAP_CONF}")
BOOTSTRAP_CONF_DIR=$(cygpath --path --windows "${BOOTSTRAP_CONF_DIR}")
BOOTSTRAP_LIBS=$(cygpath --path --windows "${BOOTSTRAP_LIBS}")
@ -220,26 +210,21 @@ run() {
fi
echo
echo "Java home: ${JAVA_HOME}"
echo "NiFi home: ${NIFI_HOME}"
echo "JAVA_HOME=${JAVA_HOME}"
echo "NIFI_HOME=${NIFI_HOME}"
echo
echo "Bootstrap Config File: ${BOOTSTRAP_CONF}"
echo
# run 'start' in the background because the process will continue to run, monitoring NiFi.
# all other commands will terminate quickly so want to just wait for them
#setup directory parameters
BOOTSTRAP_LOG_PARAMS="-Dorg.apache.nifi.bootstrap.config.log.dir='${NIFI_LOG_DIR}'"
BOOTSTRAP_PID_PARAMS="-Dorg.apache.nifi.bootstrap.config.pid.dir='${NIFI_PID_DIR}'"
BOOTSTRAP_CONF_PARAMS="-Dorg.apache.nifi.bootstrap.config.file='${BOOTSTRAP_CONF}'"
# uncomment to allow debugging of the bootstrap process
#BOOTSTRAP_DEBUG_PARAMS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000"
BOOTSTRAP_DIR_PARAMS="${BOOTSTRAP_LOG_PARAMS} ${BOOTSTRAP_PID_PARAMS} ${BOOTSTRAP_CONF_PARAMS}"
BOOTSTRAP_DIR_PARAMS="${BOOTSTRAP_LOG_PARAMS} ${BOOTSTRAP_CONF_PARAMS}"
run_bootstrap_cmd="'${JAVA}' -cp '${BOOTSTRAP_CLASSPATH}' -Xms48m -Xmx48m ${BOOTSTRAP_DIR_PARAMS} ${BOOTSTRAP_DEBUG_PARAMS} ${BOOTSTRAP_JAVA_OPTS} org.apache.nifi.bootstrap.RunNiFi"
MAXIMUM_HEAP_SIZE="-Xmx48m"
run_bootstrap_cmd="'${JAVA}' -cp '${BOOTSTRAP_CLASSPATH}' ${MAXIMUM_HEAP_SIZE} ${BOOTSTRAP_DIR_PARAMS} ${BOOTSTRAP_DEBUG_PARAMS} ${BOOTSTRAP_JAVA_OPTS} org.apache.nifi.bootstrap.BootstrapProcess"
run_nifi_cmd="${run_bootstrap_cmd} $@"
if [ -n "${run_as_user}" ]; then
@ -252,11 +237,6 @@ run() {
run_nifi_cmd="${SUDO} -u ${run_as_user} sh -c \"SCRIPT_DIR='${SCRIPT_DIR}' && . '${SCRIPT_DIR}/nifi-env.sh' && ${run_nifi_cmd}\""
fi
if [ "$1" = "run" ]; then
# Use exec to handover PID to RunNiFi java process, instead of foking it as a child process
run_nifi_cmd="exec ${run_nifi_cmd}"
fi
if [ "$1" = "set-sensitive-properties-algorithm" ]; then
run_command="'${JAVA}' -cp '${BOOTSTRAP_CLASSPATH}' '-Dnifi.properties.file.path=${NIFI_HOME}/conf/nifi.properties' 'org.apache.nifi.flow.encryptor.command.SetSensitivePropertiesAlgorithm'"
eval "cd ${NIFI_HOME}"
@ -287,8 +267,20 @@ run() {
return;
fi
if [ "$1" = "start" ]; then
( eval "cd ${NIFI_HOME} && ${run_nifi_cmd}" & )> /dev/null 1>&-
eval "cd ${NIFI_HOME}"
if [ "$1" = "run" ]; then
RUN_COMMAND=$(eval "${run_bootstrap_cmd} get-run-command")
RUN_COMMAND_STATUS=$?
if [ $RUN_COMMAND_STATUS = 0 ]; then
exec $RUN_COMMAND
else
echo "Failed to get run command"
echo "${RUN_COMMAND}"
exit 1
fi
elif [ "$1" = "start" ]; then
eval "${run_nifi_cmd}" > /dev/null 1>&- &
if [ "$2" = "--wait-for-init" ]; then
@ -304,63 +296,57 @@ run() {
time_since_feedback=0
not_running_counter=0
is_nifi_loaded="false" # 3 possible values: "true", "false", "not_running". "not_running" means NiFi has not been started.
while [ "$is_nifi_loaded" != "true" ]; do
PROCESS_STATUS=1
while [ $PROCESS_STATUS != 0 ]; do
time_at_previous_loop=$current_time
current_time=$(date +%s)
if [ "$current_time" -ge "$endtime" ]; then
echo "Exited the script due to --wait-for-init timeout"
echo "Initialization failed after $wait_timeout seconds"
break;
fi
time_since_feedback=$(($time_since_feedback+($current_time-$time_at_previous_loop)))
if [ "$time_since_feedback" -ge "$WAIT_FOR_INIT_FEEDBACK_INTERVAL" ]; then
time_since_feedback=0
echo "NiFi has not fully initialized yet..."
fi
is_nifi_loaded=$( eval "cd ${NIFI_HOME} && ${run_bootstrap_cmd} is_loaded" )
eval "cd ${NIFI_HOME} && ${run_bootstrap_cmd} status"
PROCESS_STATUS=$?
if [ "$is_nifi_loaded" = "not_running" ]; then
if [ $PROCESS_STATUS = 3 ]; then
not_running_counter=$(($not_running_counter+1))
if [ "$not_running_counter" -ge 3 ]; then
echo "NiFi is not running. Stopped waiting for it to initialize."
echo "Initialization failed"
break;
fi
fi
sleep $WAIT_FOR_INIT_SLEEP_TIME
done
if [ "$is_nifi_loaded" = "true" ]; then
echo "NiFi initialized."
echo "Exiting startup script..."
if [ $PROCESS_STATUS = 0 ]; then
echo "Initialization completed"
fi
fi
# Wait for logging initialization before returning to shell after starting
sleep 1
else
eval "cd ${NIFI_HOME} && ${run_nifi_cmd}"
eval "${run_nifi_cmd}"
fi
EXIT_STATUS=$?
# Wait just a bit (3 secs) to wait for the logging to finish and then echo a new-line.
# We do this to avoid having logs spewed on the console after running the command and then not giving
# control back to the user
sleep 1
echo
}
main() {
init "$1"
init
run "$@"
}
case "$1" in
install)
install "$@"
;;
start|stop|decommission|run|status|is_loaded|dump|diagnostics|status-history|env|set-sensitive-properties-algorithm|set-sensitive-properties-key|set-single-user-credentials|cluster-status)
start|stop|decommission|run|status|cluster-status|diagnostics|status-history|set-sensitive-properties-algorithm|set-sensitive-properties-key|set-single-user-credentials)
main "$@"
;;
@ -370,6 +356,6 @@ case "$1" in
run "start"
;;
*)
echo "Usage nifi {start|stop|decommission|run|restart|status|dump|diagnostics|status-history|set-sensitive-properties-algorithm|set-sensitive-properties-key|set-single-user-credentials|cluster-status}"
echo "Usage nifi.sh {start|stop|decommission|run|restart|status|cluster-status|diagnostics|status-history|set-sensitive-properties-algorithm|set-sensitive-properties-key|set-single-user-credentials}"
;;
esac

View File

@ -15,9 +15,6 @@
# limitations under the License.
#
# Java command to use when running NiFi
java=java
# Username to use when running NiFi. This value will be ignored on Windows.
run.as=${nifi.run.as}
@ -61,5 +58,3 @@ java.arg.securityAuthUseSubjectCredsOnly=-Djavax.security.auth.useSubjectCredsOn
# org.apache.jasper.servlet.JasperLoader,org.jvnet.hk2.internal.DelegatingClassLoader,org.apache.nifi.nar.NarClassLoader
# End of Java Agent config for native library loading.
# Port used to listen for communications from NiFi. If this property is missing, empty, or 0, a random ephemeral port is used.
nifi.bootstrap.listen.port=0

View File

@ -15,8 +15,6 @@
-->
<configuration scan="true" scanPeriod="30 seconds">
<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook" />
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>

View File

@ -1,406 +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.
*/
package org.apache.nifi;
import org.apache.nifi.cluster.ClusterDetailsFactory;
import org.apache.nifi.cluster.ConnectionState;
import org.apache.nifi.controller.DecommissionTask;
import org.apache.nifi.controller.status.history.StatusHistoryDump;
import org.apache.nifi.diagnostics.DiagnosticsDump;
import org.apache.nifi.util.LimitingInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BootstrapListener {
private static final Logger logger = LoggerFactory.getLogger(BootstrapListener.class);
private final NiFiEntryPoint nifi;
private final int bootstrapPort;
private final String secretKey;
private volatile Listener listener;
private volatile ServerSocket serverSocket;
private volatile boolean nifiLoaded = false;
public BootstrapListener(final NiFiEntryPoint nifi, final int bootstrapPort) {
this.nifi = nifi;
this.bootstrapPort = bootstrapPort;
secretKey = UUID.randomUUID().toString();
}
public void start(final int listenPort) throws IOException {
logger.debug("Starting Bootstrap Listener to communicate with Bootstrap Port {}", bootstrapPort);
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("localhost", listenPort));
serverSocket.setSoTimeout(2000);
final int localPort = serverSocket.getLocalPort();
logger.info("Started Bootstrap Listener, Listening for incoming requests on port {}", localPort);
listener = new Listener(serverSocket);
final Thread listenThread = new Thread(listener);
listenThread.setDaemon(true);
listenThread.setName("Listen to Bootstrap");
listenThread.start();
logger.debug("Notifying Bootstrap that local port is {}", localPort);
sendCommand("PORT", new String[]{String.valueOf(localPort), secretKey});
}
public void reload() throws IOException {
if (listener != null) {
listener.stop();
}
sendCommand("RELOAD", new String[]{});
}
public void stop() {
if (listener != null) {
listener.stop();
}
}
public void setNiFiLoaded(boolean nifiLoaded) {
this.nifiLoaded = nifiLoaded;
}
public void sendStartedStatus(boolean status) throws IOException {
logger.debug("Notifying Bootstrap that the status of starting NiFi is {}", status);
sendCommand("STARTED", new String[]{String.valueOf(status)});
}
private void sendCommand(final String command, final String[] args) throws IOException {
try (final Socket socket = new Socket()) {
socket.setSoTimeout(60000);
socket.connect(new InetSocketAddress("localhost", bootstrapPort));
socket.setSoTimeout(60000);
final StringBuilder commandBuilder = new StringBuilder(command);
for (final String arg : args) {
commandBuilder.append(" ").append(arg);
}
commandBuilder.append("\n");
final String commandWithArgs = commandBuilder.toString();
logger.debug("Sending command to Bootstrap: {}", commandWithArgs);
final OutputStream out = socket.getOutputStream();
out.write((commandWithArgs).getBytes(StandardCharsets.UTF_8));
out.flush();
logger.debug("Awaiting response from Bootstrap...");
final BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
final String response = reader.readLine();
if ("OK".equals(response)) {
logger.info("Successfully initiated communication with Bootstrap");
} else {
logger.error("Failed to communicate with Bootstrap. Bootstrap may be unable to issue or receive commands from NiFi");
}
}
}
private class Listener implements Runnable {
private final ServerSocket serverSocket;
private final ExecutorService executor;
private volatile boolean stopped = false;
public Listener(final ServerSocket serverSocket) {
this.serverSocket = serverSocket;
this.executor = Executors.newFixedThreadPool(2);
}
public void stop() {
stopped = true;
executor.shutdownNow();
try {
serverSocket.close();
} catch (final IOException ioe) {
// nothing to really do here. we could log this, but it would just become
// confusing in the logs, as we're shutting down and there's no real benefit
}
}
@Override
public void run() {
while (!stopped) {
try {
final Socket socket;
try {
logger.debug("Listening for Bootstrap Requests");
socket = serverSocket.accept();
} catch (final SocketTimeoutException ste) {
if (stopped) {
return;
}
continue;
} catch (final IOException ioe) {
if (stopped) {
return;
}
throw ioe;
}
logger.debug("Received connection from Bootstrap");
socket.setSoTimeout(5000);
executor.submit(new Runnable() {
@Override
public void run() {
try {
final BootstrapRequest request = readRequest(socket.getInputStream());
final BootstrapRequest.RequestType requestType = request.getRequestType();
switch (requestType) {
case PING:
logger.debug("Received PING request from Bootstrap; responding");
sendAnswer(socket.getOutputStream(), "PING");
logger.debug("Responded to PING request from Bootstrap");
break;
case RELOAD:
logger.info("Received RELOAD request from Bootstrap");
sendAnswer(socket.getOutputStream(), "RELOAD");
nifi.shutdownHook(true);
return;
case SHUTDOWN:
logger.info("Received SHUTDOWN request from Bootstrap");
sendAnswer(socket.getOutputStream(), "SHUTDOWN");
socket.close();
nifi.shutdownHook(false);
return;
case DUMP:
logger.info("Received DUMP request from Bootstrap");
writeDump(socket.getOutputStream());
break;
case CLUSTER_STATUS:
logger.info("Received CLUSTER_STATUS request from Bootstrap");
final String clusterStatus = getClusterStatus();
logger.debug("Responding to CLUSTER_STATUS request from Bootstrap with {}", clusterStatus);
sendAnswer(socket.getOutputStream(), clusterStatus);
break;
case DECOMMISSION:
logger.info("Received DECOMMISSION request from Bootstrap");
boolean shutdown = true;
final String[] decommissionArgs = request.getArgs();
if (decommissionArgs != null) {
for (final String arg : decommissionArgs) {
if ("--shutdown=false".equalsIgnoreCase(arg)) {
shutdown = false;
break;
}
}
}
logger.info("Command indicates that after decommission, shutdown={}", shutdown);
try {
decommission();
sendAnswer(socket.getOutputStream(), "DECOMMISSION");
if (shutdown) {
nifi.shutdownHook(false);
}
} catch (final Exception e) {
final OutputStream out = socket.getOutputStream();
out.write(("Failed to decommission node: " + e + "; see app-log for additional details").getBytes(StandardCharsets.UTF_8));
out.flush();
} finally {
if (shutdown) {
socket.close();
}
}
break;
case DIAGNOSTICS:
logger.info("Received DIAGNOSTICS request from Bootstrap");
final String[] args = request.getArgs();
boolean verbose = false;
if (args == null) {
verbose = false;
} else {
for (final String arg : args) {
if ("--verbose=true".equalsIgnoreCase(arg)) {
verbose = true;
break;
}
}
}
writeDiagnostics(socket.getOutputStream(), verbose);
break;
case STATUS_HISTORY:
logger.info("Received STATUS_HISTORY request from Bootstrap");
final String[] statusHistoryArgs = request.getArgs();
final int days = Integer.parseInt(statusHistoryArgs[0]);
writeNodeStatusHistory(socket.getOutputStream(), days);
break;
case IS_LOADED:
logger.debug("Received IS_LOADED request from Bootstrap");
String answer = String.valueOf(nifiLoaded);
sendAnswer(socket.getOutputStream(), answer);
logger.debug("Responded to IS_LOADED request from Bootstrap with value: {}", answer);
break;
}
} catch (final Throwable t) {
logger.error("Failed to process request from Bootstrap", t);
} finally {
try {
socket.close();
} catch (final IOException ioe) {
logger.warn("Failed to close socket to Bootstrap", ioe);
}
}
}
});
} catch (final Throwable t) {
logger.error("Failed to process request from Bootstrap", t);
}
}
}
}
private void writeDump(final OutputStream out) throws IOException {
final DiagnosticsDump diagnosticsDump = nifi.getServer().getThreadDumpFactory().create(true);
diagnosticsDump.writeTo(out);
}
private String getClusterStatus() {
final ClusterDetailsFactory clusterDetailsFactory = nifi.getServer().getClusterDetailsFactory();
if (clusterDetailsFactory == null) {
return ConnectionState.UNKNOWN.name();
}
final ConnectionState connectionState = clusterDetailsFactory.getConnectionState();
return connectionState == null ? ConnectionState.UNKNOWN.name() : connectionState.name();
}
private void decommission() throws InterruptedException {
final DecommissionTask decommissionTask = nifi.getServer().getDecommissionTask();
if (decommissionTask == null) {
throw new IllegalArgumentException("This NiFi instance does not support decommissioning");
}
decommissionTask.decommission();
}
private void writeDiagnostics(final OutputStream out, final boolean verbose) throws IOException {
final DiagnosticsDump diagnosticsDump = nifi.getServer().getDiagnosticsFactory().create(verbose);
diagnosticsDump.writeTo(out);
}
private void writeNodeStatusHistory(final OutputStream out, final int days) throws IOException {
final StatusHistoryDump statusHistoryDump = nifi.getServer().getStatusHistoryDumpFactory().create(days);
statusHistoryDump.writeTo(out);
}
private void sendAnswer(final OutputStream out, final String answer) throws IOException {
out.write((answer + "\n").getBytes(StandardCharsets.UTF_8));
out.flush();
}
@SuppressWarnings("resource") // we don't want to close the stream, as the caller will do that
private BootstrapRequest readRequest(final InputStream in) throws IOException {
// We want to ensure that we don't try to read data from an InputStream directly
// by a BufferedReader because any user on the system could open a socket and send
// a multi-gigabyte file without any new lines in order to crash the NiFi instance
// (or at least cause OutOfMemoryErrors, which can wreak havoc on the running instance).
// So we will limit the Input Stream to only 4 KB, which should be plenty for any request.
final LimitingInputStream limitingIn = new LimitingInputStream(in, 4096);
final BufferedReader reader = new BufferedReader(new InputStreamReader(limitingIn));
final String line = reader.readLine();
final String[] splits = line.split(" ");
if (splits.length < 1) {
throw new IOException("Received invalid request from Bootstrap: " + line);
}
final String requestType = splits[0];
final String[] args;
if (splits.length == 1) {
throw new IOException("Received invalid request from Bootstrap; request did not have a secret key; request type = " + requestType);
} else if (splits.length == 2) {
args = new String[0];
} else {
args = Arrays.copyOfRange(splits, 2, splits.length);
}
final String requestKey = splits[1];
if (!secretKey.equals(requestKey)) {
throw new IOException("Received invalid Secret Key for request type " + requestType);
}
try {
return new BootstrapRequest(requestType, args);
} catch (final Exception e) {
throw new IOException("Received invalid request from Bootstrap; request type = " + requestType);
}
}
private static class BootstrapRequest {
public enum RequestType {
RELOAD,
SHUTDOWN,
DUMP,
DIAGNOSTICS,
DECOMMISSION,
PING,
IS_LOADED,
STATUS_HISTORY,
CLUSTER_STATUS
}
private final RequestType requestType;
private final String[] args;
public BootstrapRequest(final String request, final String[] args) {
this.requestType = RequestType.valueOf(request);
this.args = args;
}
public RequestType getRequestType() {
return requestType;
}
@SuppressWarnings("unused")
public String[] getArgs() {
return args;
}
}
}

View File

@ -25,6 +25,8 @@ import org.apache.nifi.nar.NarUnpackMode;
import org.apache.nifi.nar.NarUnpacker;
import org.apache.nifi.nar.SystemBundle;
import org.apache.nifi.processor.DataUnit;
import org.apache.nifi.runtime.ManagementServer;
import org.apache.nifi.runtime.StandardManagementServer;
import org.apache.nifi.util.DiagnosticUtils;
import org.apache.nifi.util.FileUtils;
import org.apache.nifi.util.NiFiProperties;
@ -38,6 +40,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
@ -50,19 +53,32 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public class NiFi implements NiFiEntryPoint {
public class NiFi {
public static final String BOOTSTRAP_PORT_PROPERTY = "nifi.bootstrap.listen.port";
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
private static final String MANAGEMENT_SERVER_ADDRESS = "org.apache.nifi.management.server.address";
private static final Pattern MANAGEMENT_SERVER_ADDRESS_PATTERN = Pattern.compile("^(.+?):([1-9][0-9]{3,4})$");
private static final String MANAGEMENT_SERVER_DEFAULT_ADDRESS = "127.0.0.1:52020";
private static final int ADDRESS_GROUP = 1;
private static final int PORT_GROUP = 2;
private static final Logger LOGGER = LoggerFactory.getLogger(NiFi.class);
private final NiFiServer nifiServer;
private final BootstrapListener bootstrapListener;
private final NiFiProperties properties;
private final ManagementServer managementServer;
private volatile boolean shutdown = false;
public NiFi(final NiFiProperties properties)
@ -89,25 +105,6 @@ public class NiFi implements NiFiEntryPoint {
// register the shutdown hook
addShutdownHook();
final String bootstrapPort = System.getProperty(BOOTSTRAP_PORT_PROPERTY);
if (bootstrapPort != null) {
try {
final int port = Integer.parseInt(bootstrapPort);
if (port < 1 || port > 65535) {
throw new RuntimeException("Failed to start NiFi because system property '" + BOOTSTRAP_PORT_PROPERTY + "' is not a valid integer in the range 1 - 65535");
}
bootstrapListener = new BootstrapListener(this, port);
bootstrapListener.start(properties.getDefaultListenerBootstrapPort());
} catch (final NumberFormatException nfe) {
throw new RuntimeException("Failed to start NiFi because system property '" + BOOTSTRAP_PORT_PROPERTY + "' is not a valid integer in the range 1 - 65535");
}
} else {
LOGGER.info("NiFi started without Bootstrap Port information provided; will not listen for requests from Bootstrap");
bootstrapListener = null;
}
// delete the web working dir - if the application does not start successfully
// the web app directories might be in an invalid state. when this happens
// jetty will not attempt to re-extract the war into the directory. by removing
@ -151,15 +148,12 @@ public class NiFi implements NiFiEntryPoint {
narBundles,
extensionMapping);
managementServer = getManagementServer();
if (shutdown) {
LOGGER.info("NiFi has been shutdown via NiFi Bootstrap. Will not start Controller");
} else {
nifiServer.start();
if (bootstrapListener != null) {
bootstrapListener.setNiFiLoaded(true);
bootstrapListener.sendStartedStatus(true);
}
managementServer.start();
final long duration = System.nanoTime() - startTime;
final double durationSeconds = TimeUnit.NANOSECONDS.toMillis(duration) / 1000.0;
@ -172,14 +166,16 @@ public class NiFi implements NiFiEntryPoint {
}
protected void setDefaultUncaughtExceptionHandler() {
Thread.setDefaultUncaughtExceptionHandler((thread, exception) -> LOGGER.error("An Unknown Error Occurred in Thread {}", thread, exception));
Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler());
}
protected void addShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(() ->
// shutdown the jetty server
shutdownHook(false)
));
final Thread shutdownHook = Thread.ofPlatform()
.name(NiFi.class.getSimpleName())
.uncaughtExceptionHandler(new ExceptionHandler())
.unstarted(this::stop);
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
protected void initLogging() {
@ -205,12 +201,15 @@ public class NiFi implements NiFiEntryPoint {
return new URLClassLoader(urls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader());
}
public void shutdownHook(final boolean isReload) {
/**
* Stop Application and shutdown server
*/
public void stop() {
try {
runDiagnosticsOnShutdown();
shutdown();
} catch (final Throwable t) {
LOGGER.warn("Problem occurred ensuring Jetty web server was properly terminated", t);
LOGGER.warn("Application Controller shutdown failed", t);
}
}
@ -238,18 +237,39 @@ public class NiFi implements NiFiEntryPoint {
}
}
protected void shutdown() {
this.shutdown = true;
LOGGER.info("Application Server shutdown started");
if (nifiServer != null) {
LOGGER.info("Application Controller shutdown started");
managementServer.stop();
if (nifiServer == null) {
LOGGER.info("Application Server not running");
} else {
nifiServer.stop();
}
if (bootstrapListener != null) {
bootstrapListener.stop();
LOGGER.info("Application Controller shutdown completed");
}
private ManagementServer getManagementServer() {
final String managementServerAddressProperty = System.getProperty(MANAGEMENT_SERVER_ADDRESS, MANAGEMENT_SERVER_DEFAULT_ADDRESS);
if (managementServerAddressProperty.isBlank()) {
throw new IllegalStateException("Management Server Address System Property [%s] not configured".formatted(MANAGEMENT_SERVER_ADDRESS));
}
final Matcher matcher = MANAGEMENT_SERVER_ADDRESS_PATTERN.matcher(managementServerAddressProperty);
if (matcher.matches()) {
final String addressGroup = matcher.group(ADDRESS_GROUP);
final String portGroup = matcher.group(PORT_GROUP);
final int port = Integer.parseInt(portGroup);
final InetSocketAddress bindAddress = new InetSocketAddress(addressGroup, port);
return new StandardManagementServer(bindAddress, nifiServer);
} else {
throw new IllegalStateException("Management Server Address System Property [%s] not valid [%s]".formatted(MANAGEMENT_SERVER_ADDRESS, managementServerAddressProperty));
}
LOGGER.info("Application Server shutdown completed");
}
/**
@ -301,4 +321,12 @@ public class NiFi implements NiFiEntryPoint {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
}
private static class ExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(final Thread thread, Throwable exception) {
LOGGER.error("An Unknown Error Occurred in Thread {}", thread, exception);
}
}
}

View File

@ -0,0 +1,106 @@
/*
* 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.runtime;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import org.apache.nifi.NiFiServer;
import org.apache.nifi.cluster.ClusterDetailsFactory;
import org.apache.nifi.cluster.ConnectionState;
import org.apache.nifi.controller.DecommissionTask;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import static java.net.HttpURLConnection.HTTP_ACCEPTED;
import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
import static java.net.HttpURLConnection.HTTP_OK;
/**
* HTTP Handler for Cluster Health status operations
*/
class HealthClusterHttpHandler implements HttpHandler {
private static final String CONTENT_TYPE_HEADER = "Content-Type";
private static final String TEXT_PLAIN = "text/plain";
private static final int NO_RESPONSE_BODY = -1;
private static final String GET_METHOD = "GET";
private static final String DELETE_METHOD = "DELETE";
private static final String STATUS = "Cluster Status: %s\n";
private final NiFiServer server;
HealthClusterHttpHandler(final NiFiServer server) {
this.server = Objects.requireNonNull(server);
}
@Override
public void handle(final HttpExchange exchange) throws IOException {
final String requestMethod = exchange.getRequestMethod();
final OutputStream responseBody = exchange.getResponseBody();
if (GET_METHOD.contentEquals(requestMethod)) {
exchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, TEXT_PLAIN);
final ConnectionState connectionState = getConnectionState();
final String status = STATUS.formatted(connectionState);
final byte[] response = status.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(HTTP_OK, response.length);
responseBody.write(response);
} else if (DELETE_METHOD.contentEquals(requestMethod)) {
startDecommission();
exchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, TEXT_PLAIN);
final String status = STATUS.formatted(ConnectionState.OFFLOADING);
final byte[] response = status.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(HTTP_ACCEPTED, response.length);
responseBody.write(response);
} else {
exchange.sendResponseHeaders(HTTP_BAD_METHOD, NO_RESPONSE_BODY);
}
}
private void startDecommission() {
final DecommissionTask decommissionTask = server.getDecommissionTask();
Thread.ofVirtual().name(DecommissionTask.class.getSimpleName()).start(() -> {
try {
decommissionTask.decommission();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
private ConnectionState getConnectionState() {
final ConnectionState connectionState;
final ClusterDetailsFactory clusterDetailsFactory = server.getClusterDetailsFactory();
if (clusterDetailsFactory == null) {
connectionState = ConnectionState.UNKNOWN;
} else {
connectionState = clusterDetailsFactory.getConnectionState();
}
return connectionState;
}
}

View File

@ -0,0 +1,88 @@
/*
* 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.runtime;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import org.apache.nifi.NiFiServer;
import org.apache.nifi.diagnostics.DiagnosticsDump;
import org.apache.nifi.diagnostics.DiagnosticsFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.Objects;
import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
import static java.net.HttpURLConnection.HTTP_OK;
/**
* HTTP Handler for Health Diagnostics operations
*/
class HealthDiagnosticsHttpHandler implements HttpHandler {
private static final String CONTENT_TYPE_HEADER = "Content-Type";
private static final String TEXT_PLAIN = "text/plain";
private static final int STREAM_RESPONSE_BODY = 0;
private static final int NO_RESPONSE_BODY = -1;
private static final String GET_METHOD = "GET";
private static final String VERBOSE_QUERY_ENABLED = "verbose=true";
private final NiFiServer server;
HealthDiagnosticsHttpHandler(final NiFiServer server) {
this.server = Objects.requireNonNull(server);
}
@Override
public void handle(final HttpExchange exchange) throws IOException {
final String requestMethod = exchange.getRequestMethod();
if (GET_METHOD.contentEquals(requestMethod)) {
exchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, TEXT_PLAIN);
exchange.sendResponseHeaders(HTTP_OK, STREAM_RESPONSE_BODY);
final URI requestUri = exchange.getRequestURI();
final boolean verboseRequested = getVerboseRequested(requestUri);
final DiagnosticsFactory diagnosticsFactory = server.getDiagnosticsFactory();
final DiagnosticsDump diagnosticsDump = diagnosticsFactory.create(verboseRequested);
try (OutputStream responseBody = exchange.getResponseBody()) {
diagnosticsDump.writeTo(responseBody);
}
} else {
exchange.sendResponseHeaders(HTTP_BAD_METHOD, NO_RESPONSE_BODY);
}
}
private boolean getVerboseRequested(final URI requestUri) {
final boolean verboseRequested;
final String query = requestUri.getQuery();
if (query == null) {
verboseRequested = false;
} else {
verboseRequested = query.contains(VERBOSE_QUERY_ENABLED);
}
return verboseRequested;
}
}

View File

@ -0,0 +1,58 @@
/*
* 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.runtime;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
import static java.net.HttpURLConnection.HTTP_OK;
/**
* HTTP Handler for Health status operations
*/
class HealthHttpHandler implements HttpHandler {
private static final String CONTENT_TYPE_HEADER = "Content-Type";
private static final String TEXT_PLAIN = "text/plain";
private static final int NO_RESPONSE_BODY = -1;
private static final String GET_METHOD = "GET";
private static final String STATUS_UP = "Status: UP\n";
@Override
public void handle(final HttpExchange exchange) throws IOException {
final String requestMethod = exchange.getRequestMethod();
final OutputStream responseBody = exchange.getResponseBody();
if (GET_METHOD.contentEquals(requestMethod)) {
exchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, TEXT_PLAIN);
final byte[] response = STATUS_UP.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(HTTP_OK, response.length);
responseBody.write(response);
} else {
exchange.sendResponseHeaders(HTTP_BAD_METHOD, NO_RESPONSE_BODY);
}
}
}

View File

@ -0,0 +1,101 @@
/*
* 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.runtime;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import org.apache.nifi.NiFiServer;
import org.apache.nifi.controller.status.history.StatusHistoryDump;
import org.apache.nifi.controller.status.history.StatusHistoryDumpFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
import static java.net.HttpURLConnection.HTTP_OK;
/**
* HTTP Handler for Health Status History operations
*/
class HealthStatusHistoryHttpHandler implements HttpHandler {
private static final String CONTENT_TYPE_HEADER = "Content-Type";
private static final String APPLICATION_JSON = "application/json";
private static final int STREAM_RESPONSE_BODY = 0;
private static final int NO_RESPONSE_BODY = -1;
private static final String GET_METHOD = "GET";
private static final Pattern DAYS_QUERY_PATTERN = Pattern.compile("^days=(\\d+)$");
private static final int DAYS_GROUP = 1;
private static final int DAYS_DEFAULT = 1;
private final NiFiServer server;
HealthStatusHistoryHttpHandler(final NiFiServer server) {
this.server = Objects.requireNonNull(server);
}
@Override
public void handle(final HttpExchange exchange) throws IOException {
final String requestMethod = exchange.getRequestMethod();
if (GET_METHOD.contentEquals(requestMethod)) {
exchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, APPLICATION_JSON);
exchange.sendResponseHeaders(HTTP_OK, STREAM_RESPONSE_BODY);
final URI requestUri = exchange.getRequestURI();
final int daysRequested = getDaysRequested(requestUri);
final StatusHistoryDumpFactory statusHistoryDumpFactory = server.getStatusHistoryDumpFactory();
final StatusHistoryDump statusHistoryDump = statusHistoryDumpFactory.create(daysRequested);
try (OutputStream responseBody = exchange.getResponseBody()) {
statusHistoryDump.writeTo(responseBody);
}
} else {
exchange.sendResponseHeaders(HTTP_BAD_METHOD, NO_RESPONSE_BODY);
}
}
private int getDaysRequested(final URI requestUri) {
final int daysRequested;
final String query = requestUri.getQuery();
if (query == null) {
daysRequested = DAYS_DEFAULT;
} else {
final Matcher matcher = DAYS_QUERY_PATTERN.matcher(query);
if (matcher.matches()) {
final String daysGroup = matcher.group(DAYS_GROUP);
daysRequested = Integer.parseInt(daysGroup);
} else {
daysRequested = DAYS_DEFAULT;
}
}
return daysRequested;
}
}

View File

@ -0,0 +1,32 @@
/*
* 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.runtime;
/**
* Abstraction for controlling Management Server operation
*/
public interface ManagementServer {
/**
* Start Server and bind to configured address
*/
void start();
/**
* Stop Server
*/
void stop();
}

View File

@ -0,0 +1,97 @@
/*
* 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.runtime;
import com.sun.net.httpserver.HttpServer;
import org.apache.nifi.NiFiServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.util.Objects;
/**
* Standard Management Server based on Java HttpServer
*/
public class StandardManagementServer implements ManagementServer {
private static final Logger logger = LoggerFactory.getLogger(StandardManagementServer.class);
private static final String HEALTH_PATH = "/health";
private static final String HEALTH_CLUSTER_PATH = "/health/cluster";
private static final String HEALTH_DIAGNOSTICS_PATH = "/health/diagnostics";
private static final String HEALTH_STATUS_HISTORY_PATH = "/health/status-history";
private static final int STOP_DELAY = 0;
private static final int CONNECTION_BACKLOG = 10;
private final InetSocketAddress bindAddress;
private final NiFiServer server;
private HttpServer httpServer;
public StandardManagementServer(final InetSocketAddress bindAddress, final NiFiServer server) {
this.bindAddress = Objects.requireNonNull(bindAddress, "Bind Address required");
this.server = Objects.requireNonNull(server, "Server required");
}
@Override
public void start() {
if (httpServer == null) {
try {
httpServer = HttpServer.create();
httpServer.createContext(HEALTH_PATH, new HealthHttpHandler());
httpServer.createContext(HEALTH_CLUSTER_PATH, new HealthClusterHttpHandler(server));
httpServer.createContext(HEALTH_DIAGNOSTICS_PATH, new HealthDiagnosticsHttpHandler(server));
httpServer.createContext(HEALTH_STATUS_HISTORY_PATH, new HealthStatusHistoryHttpHandler(server));
httpServer.bind(bindAddress, CONNECTION_BACKLOG);
httpServer.start();
final InetSocketAddress serverAddress = getServerAddress();
logger.info("Started Management Server on http://{}:{}", serverAddress.getHostString(), serverAddress.getPort());
} catch (final IOException e) {
throw new UncheckedIOException("Management Server start failed", e);
}
} else {
throw new IllegalStateException("Management Server running");
}
}
@Override
public void stop() {
if (httpServer == null) {
logger.info("Management Server not running");
} else {
httpServer.stop(STOP_DELAY);
logger.info("Management Server stopped");
}
}
protected InetSocketAddress getServerAddress() {
return httpServer == null ? bindAddress : httpServer.getAddress();
}
}

View File

@ -0,0 +1,121 @@
/*
* 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.runtime;
import org.apache.nifi.NiFiServer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
@ExtendWith(MockitoExtension.class)
class StandardManagementServerTest {
private static final String LOCALHOST = "127.0.0.1";
private static final String HEALTH_URI = "http://%s:%d/health";
private static final String GET_METHOD = "GET";
private static final String DELETE_METHOD = "DELETE";
private static final Duration TIMEOUT = Duration.ofSeconds(15);
@Mock
private NiFiServer server;
@Test
void testStartStop() {
final InetSocketAddress initialBindAddress = new InetSocketAddress(LOCALHOST, 0);
final StandardManagementServer standardManagementServer = new StandardManagementServer(initialBindAddress, server);
try {
standardManagementServer.start();
final InetSocketAddress bindAddress = standardManagementServer.getServerAddress();
assertNotSame(initialBindAddress.getPort(), bindAddress.getPort());
} finally {
standardManagementServer.stop();
}
}
@Test
void testGetHealth() throws Exception {
final InetSocketAddress initialBindAddress = new InetSocketAddress(LOCALHOST, 0);
final StandardManagementServer standardManagementServer = new StandardManagementServer(initialBindAddress, server);
try {
standardManagementServer.start();
final InetSocketAddress serverAddress = standardManagementServer.getServerAddress();
assertNotSame(initialBindAddress.getPort(), serverAddress.getPort());
assertResponseStatusCode(serverAddress, GET_METHOD, HttpURLConnection.HTTP_OK);
} finally {
standardManagementServer.stop();
}
}
@Test
void testDeleteHealth() throws Exception {
final InetSocketAddress initialBindAddress = new InetSocketAddress(LOCALHOST, 0);
final StandardManagementServer standardManagementServer = new StandardManagementServer(initialBindAddress, server);
try {
standardManagementServer.start();
final InetSocketAddress serverAddress = standardManagementServer.getServerAddress();
assertNotSame(initialBindAddress.getPort(), serverAddress.getPort());
assertResponseStatusCode(serverAddress, DELETE_METHOD, HttpURLConnection.HTTP_BAD_METHOD);
} finally {
standardManagementServer.stop();
}
}
private void assertResponseStatusCode(final InetSocketAddress serverAddress, final String method, final int responseStatusCode) throws IOException, InterruptedException {
final URI healthUri = URI.create(HEALTH_URI.formatted(serverAddress.getHostString(), serverAddress.getPort()));
try (HttpClient httpClient = HttpClient.newBuilder().connectTimeout(TIMEOUT).build()) {
final HttpRequest request = HttpRequest.newBuilder(healthUri)
.method(method, HttpRequest.BodyPublishers.noBody())
.timeout(TIMEOUT)
.build();
final HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
final int statusCode = response.statusCode();
assertEquals(responseStatusCode, statusCode);
final String responseBody = response.body();
assertNotNull(responseBody);
}
}
}

View File

@ -16,7 +16,12 @@
*/
package org.apache.nifi.tests.system;
import org.apache.nifi.bootstrap.RunNiFi;
import org.apache.nifi.bootstrap.command.process.ManagementServerAddressProvider;
import org.apache.nifi.bootstrap.command.process.ProcessBuilderProvider;
import org.apache.nifi.bootstrap.command.process.StandardManagementServerAddressProvider;
import org.apache.nifi.bootstrap.command.process.StandardProcessBuilderProvider;
import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
import org.apache.nifi.bootstrap.configuration.StandardConfigurationProvider;
import org.apache.nifi.registry.security.util.KeystoreType;
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClient;
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientConfig;
@ -51,7 +56,7 @@ public class SpawnedStandaloneNiFiInstanceFactory implements NiFiInstanceFactory
@Override
public NiFiInstance createInstance() {
return new RunNiFiInstance(instanceConfig);
return new ProcessNiFiInstance(instanceConfig);
}
@Override
@ -78,14 +83,14 @@ public class SpawnedStandaloneNiFiInstanceFactory implements NiFiInstanceFactory
return Objects.hash(instanceConfig);
}
private static class RunNiFiInstance implements NiFiInstance {
private static class ProcessNiFiInstance implements NiFiInstance {
private final File instanceDirectory;
private final File configDir;
private final InstanceConfiguration instanceConfiguration;
private File bootstrapConfigFile;
private RunNiFi runNiFi;
private Process process;
public RunNiFiInstance(final InstanceConfiguration instanceConfiguration) {
public ProcessNiFiInstance(final InstanceConfiguration instanceConfiguration) {
this.instanceDirectory = instanceConfiguration.getInstanceDirectory();
this.bootstrapConfigFile = instanceConfiguration.getBootstrapConfigFile();
this.instanceConfiguration = instanceConfiguration;
@ -108,30 +113,33 @@ public class SpawnedStandaloneNiFiInstanceFactory implements NiFiInstanceFactory
@Override
public String toString() {
return "RunNiFiInstance[dir=" + instanceDirectory + "]";
return "ProcessNiFiInstance[home=%s,process=%s]".formatted(instanceDirectory, process);
}
@Override
public void start(final boolean waitForCompletion) {
if (runNiFi != null) {
if (process != null) {
throw new IllegalStateException("NiFi has already been started");
}
logger.info("Starting NiFi [{}]", instanceDirectory.getName());
try {
this.runNiFi = new RunNiFi(bootstrapConfigFile);
} catch (IOException e) {
throw new RuntimeException("Failed to start NiFi", e);
}
final Map<String, String> environmentVariables = Map.of("NIFI_HOME", instanceDirectory.getAbsolutePath());
final ConfigurationProvider configurationProvider = new StandardConfigurationProvider(environmentVariables, new Properties());
final ManagementServerAddressProvider managementServerAddressProvider = new StandardManagementServerAddressProvider(configurationProvider);
final ProcessBuilderProvider processBuilderProvider = new StandardProcessBuilderProvider(configurationProvider, managementServerAddressProvider);
try {
runNiFi.start(false);
final ProcessBuilder processBuilder = processBuilderProvider.getApplicationProcessBuilder();
processBuilder.directory(instanceDirectory);
process = processBuilder.start();
logger.info("Started NiFi [{}] PID [{}]", instanceDirectory.getName(), process.pid());
if (waitForCompletion) {
waitForStartup();
}
} catch (IOException e) {
} catch (final IOException e) {
throw new RuntimeException("Failed to start NiFi", e);
}
}
@ -225,7 +233,7 @@ public class SpawnedStandaloneNiFiInstanceFactory implements NiFiInstanceFactory
@Override
public boolean isAccessible() {
if (runNiFi == null) {
if (process == null) {
return false;
}
@ -263,20 +271,27 @@ public class SpawnedStandaloneNiFiInstanceFactory implements NiFiInstanceFactory
@Override
public void stop() {
if (runNiFi == null) {
if (process == null) {
logger.info("NiFi Shutdown Ignored (runNiFi==null) [{}]", instanceDirectory.getName());
return;
}
logger.info("NiFi Shutdown Started [{}]", instanceDirectory.getName());
logger.info("NiFi Process [{}] Shutdown Started [{}]", process.pid(), instanceDirectory.getName());
try {
runNiFi.stop();
logger.info("NiFi Shutdown Completed [{}]", instanceDirectory.getName());
} catch (IOException e) {
process.destroy();
logger.info("NiFi Process [{}] Shutdown Requested [{}]", process.pid(), instanceDirectory.getName());
process.waitFor(15, TimeUnit.SECONDS);
logger.info("NiFi Process [{}] Shutdown Completed [{}]", process.pid(), instanceDirectory.getName());
} catch (final Exception e) {
throw new RuntimeException("Failed to stop NiFi", e);
} finally {
runNiFi = null;
try {
process.destroyForcibly();
} catch (final Exception e) {
logger.warn("NiFi Process [{}] force termination failed", process.pid(), e);
}
process = null;
}
}
@ -352,11 +367,8 @@ public class SpawnedStandaloneNiFiInstanceFactory implements NiFiInstanceFactory
copyContents(new File(getInstanceDirectory(), dirToCopy), new File(destinationDir, dirToCopy));
}
if (runNiFi == null) {
if (process == null) {
logger.warn("NiFi instance is not running so will not capture diagnostics for {}", getInstanceDirectory());
} else {
final File diagnosticsFile = new File(destinationDir, "diagnostics.txt");
runNiFi.diagnostics(diagnosticsFile, false);
}
final File causeFile = new File(destinationDir, "test-failure-stack-trace.txt");

View File

@ -15,8 +15,6 @@
-->
<configuration scan="true" scanPeriod="30 seconds">
<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook" />
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>

View File

@ -15,8 +15,6 @@
-->
<configuration scan="true" scanPeriod="30 seconds">
<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook" />
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>

View File

@ -15,8 +15,6 @@
-->
<configuration scan="true" scanPeriod="30 seconds">
<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook" />
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>

View File

@ -15,8 +15,6 @@
-->
<configuration scan="true" scanPeriod="30 seconds">
<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook" />
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>

View File

@ -129,7 +129,7 @@
<org.apache.httpcomponents.httpcore.version>4.4.16</org.apache.httpcomponents.httpcore.version>
<org.bouncycastle.version>1.78.1</org.bouncycastle.version>
<testcontainers.version>1.20.1</testcontainers.version>
<org.slf4j.version>2.0.15</org.slf4j.version>
<org.slf4j.version>2.0.16</org.slf4j.version>
<com.jayway.jsonpath.version>2.9.0</com.jayway.jsonpath.version>
<derby.version>10.17.1.0</derby.version>
<jetty.version>12.0.12</jetty.version>