/*******************************************************************************
* Copyright (c) 2015, 2016 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat - Initial Contribution
*******************************************************************************/
package org.eclipse.linuxtools.internal.docker.ui.jobs;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.linuxtools.docker.core.DockerException;
import org.eclipse.linuxtools.docker.core.DockerImageBuildFailedException;
import org.eclipse.linuxtools.docker.core.IDockerConnection;
import org.eclipse.linuxtools.docker.core.IDockerImage;
import org.eclipse.linuxtools.docker.core.IDockerProgressHandler;
import org.eclipse.linuxtools.docker.core.IDockerProgressMessage;
import org.eclipse.linuxtools.docker.ui.Activator;
import org.eclipse.linuxtools.internal.docker.core.DockerConnection;
import org.eclipse.linuxtools.internal.docker.ui.consoles.BuildConsole;
import org.eclipse.linuxtools.internal.docker.ui.launch.IBuildDockerImageLaunchConfigurationConstants;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
/**
* A {@link Job} to call and progressMonitor the build of an
* {@link IDockerImage}
*
*/
public class BuildDockerImageJob extends Job implements IDockerProgressHandler {
private static final String BUILD_IMAGE_JOB_TITLE = "BuildImageJob.title"; //$NON-NLS-1$
private static final String BUILD_IMAGE_ERROR_MESSAGE = "BuildImageError.msg"; //$NON-NLS-1$
private static final String DOCKERFILE_LINE_COUNT_ERROR = "ImageBuildError.msg"; //$NON-NLS-1$
private static final String SKIP_EMPTY_DOCKERFILE = "SkipEmptydockerfile.msg"; //$NON-NLS-1$
/** The {@link IDockerConnection} to use. */
private final IDockerConnection connection;
/** The path to the source code. */
private final IPath path;
/** the build options. */
private final Map<String, Object> buildOptions;
/** the optional repoName (i.e., repo[tag]) for the image to build */
private final String repoName;
/** The console used to display build output messages. */
private final BuildConsole console;
/** The progress progressMonitor associated with this {@link Job}. */
private IProgressMonitor progressMonitor;
/**
* Constructor
*
* @param connection
* the Docker connection to use (i.e., on which Docker engine to
* build the image)
* @param path
* the path to the source code
* @param repoName
* the optional repoName (i.e., repo[tag]) for the image to build
* @param buildOptions
* build options
* @throws DockerException
* @throws IOException
* @see {@link IBuildDockerImageLaunchConfigurationConstants} for build
* options.
*/
public BuildDockerImageJob(final IDockerConnection connection,
final IPath path, final String repoName,
final Map<String, Object> buildOptions) {
super(JobMessages.getString(BUILD_IMAGE_JOB_TITLE));
this.connection = connection;
this.path = path;
this.repoName = repoName;
this.buildOptions = buildOptions;
this.console = BuildConsole.findConsole();
}
/**
* Verifies that the file located at the given {@code pathToDockerfile}
* exists and is readable.
*
* @param pathToDockerfile
* the path to the Docker file
* @return
*/
private static boolean verifyPathToDockerfile(
final IPath pathToDockerfile) {
final File dockerfile = pathToDockerfile.toFile();
if (!dockerfile.exists() || !dockerfile.canRead()) {
Display.getDefault().asyncExec(() -> {
MessageDialog.openError(Display.getDefault().getActiveShell(),
JobMessages.getString("BuildImageJob.title"), //$NON-NLS-1$
JobMessages.getFormattedString(
"BuildImageError.missing.dockerfile.msg", //$NON-NLS-1$
dockerfile.getAbsolutePath()));
});
return false;
}
return true;
}
@Override
protected IStatus run(final IProgressMonitor progressMonitor) {
try {
this.progressMonitor = progressMonitor;
final IPath pathToDockerfile = path.addTrailingSeparator()
.append("Dockerfile"); //$NON-NLS-1$
if (verifyPathToDockerfile(pathToDockerfile)) {
final int numberOfBuildOperations = countLines(
pathToDockerfile.toOSString()); // $NON-NLS-1$
if (numberOfBuildOperations == 0) {
Activator.log(new Status(IStatus.WARNING,
Activator.PLUGIN_ID,
JobMessages.getString(SKIP_EMPTY_DOCKERFILE)));
} else {
this.console.clearConsole();
this.console.activate();
this.progressMonitor.beginTask(
JobMessages.getString(BUILD_IMAGE_JOB_TITLE),
numberOfBuildOperations + 1);
if (repoName == null) {
// Give the Image a default name so it can be tagged
// later.
// Otherwise, the Image will be treated as an
// intermediate
// Image
// by the view filters and Tag Image action will be
// disabled.
// Use the current time in milliseconds to make it
// unique.
final String name = "dockerfile:" //$NON-NLS-1$
+ Long.toHexString(System.currentTimeMillis());
((DockerConnection) connection).buildImage(this.path,
name, this, this.buildOptions);
} else {
((DockerConnection) connection).buildImage(this.path,
this.repoName, this, this.buildOptions);
}
connection.getImages(true);
}
}
} catch (DockerException | InterruptedException e) {
Display.getDefault()
.syncExec(() -> MessageDialog.openError(
PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getShell(),
JobMessages.getString(BUILD_IMAGE_ERROR_MESSAGE),
e.getMessage()));
} finally {
// make sure the progress monitor is 'done' even if the build failed
// or
// timed out.
this.progressMonitor.done();
}
return Status.OK_STATUS;
}
@Override
public void processMessage(final IDockerProgressMessage message)
throws DockerException {
if (message.error() != null) {
cancel();
throw new DockerImageBuildFailedException(message.error());
}
// For imageName build, all the data is in the stream.
final String status = message.stream();
if (status != null && status.startsWith("Successfully built")) { //$NON-NLS-1$
// refresh images
connection.getImages(true);
} else if (status != null && status.startsWith("Step")) { //$NON-NLS-1$
this.progressMonitor.worked(1);
}
logMessage(status);
}
private void logMessage(final String buildMessage) {
if (this.console != null && buildMessage != null) {
Display.getDefault().asyncExec(() -> {
console.showConsole();
try {
console.write(buildMessage.getBytes("UTF-8")); //$NON-NLS-1$
} catch (IOException e) {
Activator.log(e);
}
});
}
}
/**
* Counts the number of lines in the given Docker build file that contain
* statements to execute (ignoring comments and empty lines).
*
* @param fileName
* the full repoName of the Docker file to read
* @return the number of instructions.
* @throws DockerException
* @throws IOException
*/
private static int countLines(final String fileName)
throws DockerException {
try {
int count = 0;
try (final InputStream fis = new FileInputStream(fileName);
final InputStreamReader isr = new InputStreamReader(fis);
final BufferedReader br = new BufferedReader(isr);) {
String line;
while ((line = br.readLine()) != null) {
// ignore empty lines and comments
if (line.startsWith("#") || line.trim().isEmpty()) { //$NON-NLS-1$
continue;
}
count++;
}
}
return count;
} catch (IOException e) {
throw new DockerException(
JobMessages.getString(DOCKERFILE_LINE_COUNT_ERROR), e);
}
}
}