/**
* Copyright 2014-2017 Linagora, Université Joseph Fourier, Floralis
*
* The present code is developed in the scope of the joint LINAGORA -
* Université Joseph Fourier - Floralis research program and is designated
* as a "Result" pursuant to the terms and conditions of the LINAGORA
* - Université Joseph Fourier - Floralis research program. Each copyright
* holder of Results enumerated here above fully & independently holds complete
* ownership of the complete Intellectual Property rights applicable to the whole
* of said Results, and may freely exploit it in any manner which does not infringe
* the moral rights of the other copyright holders.
*
* Licensed 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 net.roboconf.target.docker.internal;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import org.ops4j.pax.url.mvn.MavenResolver;
import net.roboconf.core.utils.UriUtils;
import net.roboconf.core.utils.Utils;
/**
* Generate a Dockerfile directory with all necessary stuff to setup a Roboconf agent.
* @author Pierre-Yves Gibello - Linagora
* @author Pierre Bourret - Université Joseph Fourier
*/
public class DockerfileGenerator {
static final String RBCF_DIR = "/usr/local/roboconf-agent/";
static final String BACKUP = "agent-extensions-backup/";
private static final String DEPLOY = "deploy/";
private static final String GENERATED_FEATURE_XML = "generated-feature.xml";
private static final String[] RESOURCES_TO_COPY = { "start.sh", "rename.sh", "rc.local" };
private final Logger logger = Logger.getLogger( getClass().getName());
private final String agentPackURL;
private String packages = DockerHandler.AGENT_JRE_AND_PACKAGES_DEFAULT;
private boolean isTar = true;
private String baseImageName = "ubuntu:14.04";
final List<String> deployList;
final List<String> bundleUrls = new ArrayList<> ();
MavenResolver mavenResolver;
/**
* Constructor for docker file generator.
* @param agentPackURL URL or path to the agent tarball or zip (not null)
* @param packages packages to be installed using apt-get (including JRE)
* <p>
* Set to "openjdk-7-jre-headless" if null.
* </p>
* @param deployList a list of URLs of additional resources to deploy.
* @param baseImageName the name of the base image used to create a new image
* <p>
* This parameter can be null.<br>
* In this case, "ubuntu" will be used as the
* base image name (<code>FROM ubuntu</code>).
* </p>
*/
public DockerfileGenerator(
String agentPackURL,
String packages,
List<String> deployList,
String baseImageName ) {
File test = new File(agentPackURL);
this.agentPackURL = (test.exists() ? "file://" : "") + agentPackURL;
if( ! Utils.isEmptyOrWhitespaces( packages ))
this.packages = packages;
if( ! Utils.isEmptyOrWhitespaces( baseImageName ))
this.baseImageName = baseImageName;
if(agentPackURL.toLowerCase().endsWith("zip"))
this.isTar = false;
this.deployList = deployList;
}
/**
* @param mavenResolver the mavenResolver to set
*/
public void setMavenResolver( MavenResolver mavenResolver ) {
this.mavenResolver = mavenResolver;
}
/**
* Generates a dockerfile.
* @return path to a full-fledged temporary Dockerfile directory
* @throws IOException
* @throws URISyntaxException
*/
public File generateDockerfile() throws IOException, URISyntaxException {
// Create temporary dockerfile directory
Path dockerfile = Files.createTempDirectory("roboconf_");
// Copy agent package in temporary dockerfile directory
String agentFilename = findAgentFileName( this.agentPackURL, this.isTar );
File tmpPack = new File( dockerfile.toFile(), agentFilename );
tmpPack.setReadable( true );
try {
this.logger.fine( "Downloading the agent package from " + this.agentPackURL );
DockerUtils.downloadRemotePackage( this.agentPackURL, tmpPack, this.mavenResolver );
this.logger.fine( "The agent package was successfully downloaded." );
} catch( Exception e ) {
this.logger.fine( "The agent package could not be downloaded." );
Utils.logException( this.logger, e );
throw e instanceof IOException ? (IOException) e : new IOException( e );
}
// Generate the Docker instructions
this.logger.fine( "Generating a Dockerfile." );
File generated = new File( dockerfile.toFile(), "Dockerfile" );
PrintWriter out = null;
try {
out = new PrintWriter( generated, "UTF-8" );
out.println("FROM " + this.baseImageName);
String actualPackages = this.packages;
if( ! this.isTar )
actualPackages = "unzip " + actualPackages;
out.println("RUN apt-get update && apt-get install -y " + actualPackages + " && rm -rf /var/lib/apt/lists/*");
// Copy and unzip the agent archive.
out.println("COPY " + agentFilename + " /usr/local/");
out.println("RUN cd /usr/local; " + (this.isTar ? "tar xvzf " : "unzip ") + agentFilename);
// We used to assume the name of the extracted directory was the name of the ZIP file
// without any file extension. This is wrong. If one points to a snapshot version (e.g. hosted on Sonatype)
// then the file name contains a qualifier while the inner directory contains the SNAPSHOT mention.
// The only assumption we can do is that it starts with "roboconf-something-agent".
// We will rename this directory to "roboconf-agent".
out.println("COPY rename.sh /usr/local/");
out.println("RUN cd /usr/local; ./rename.sh");
// The rc.local and start.sh files will be generated as well!
out.println("COPY rc.local /etc/");
out.println("COPY start.sh " + RBCF_DIR);
// Additional agent extensions?
if( this.deployList != null ) {
out.println( "RUN mkdir -p " + RBCF_DIR + BACKUP );
out.println( this.handleAdditionalDeployments( dockerfile.toFile()));
}
// Generated feature?
// It must be named "feature.xml"!!!
if( this.bundleUrls.size() > 0 )
out.println( "COPY " + GENERATED_FEATURE_XML + " " + RBCF_DIR + DEPLOY + "feature.xml" );
} finally {
Utils.closeQuietly( out );
this.logger.fine( "The Dockerfile was generated." );
}
// Copy essential resources in the Dockerfile
for( final String s : RESOURCES_TO_COPY ) {
this.logger.fine( "Copying " + s + "..." );
generated = new File( dockerfile.toFile(), s );
InputStream in = null;
try {
in = this.getClass().getResourceAsStream( "/" + s );
Utils.copyStream( in, generated );
generated.setExecutable( true, false );
this.logger.fine( s + " was copied within the Dockerfile's directory." );
} finally {
Utils.closeQuietly( in );
}
}
// Do we have bundles to deploy?
// If some, generate a feature and save it in the Dockerfile resources.
String karafFeature = prepareKarafFeature( this.bundleUrls );
if( karafFeature != null ) {
this.logger.fine( "Writing " + GENERATED_FEATURE_XML + "..." );
File target = new File( dockerfile.toFile(), GENERATED_FEATURE_XML );
Utils.writeStringInto( karafFeature, target );
this.logger.fine( GENERATED_FEATURE_XML + " was copied within the Dockerfile's directory. (" + target + ")" );
}
return dockerfile.toFile();
}
/**
* Retrieve packages list (for apt-get).
* @return The packages list
*/
public String getPackages() {
return this.packages;
}
/**
* Determine if the agent package is a tarball (tar/tgz) file.
* @return true for a tarball, false otherwise
*/
public boolean isTar() {
return this.isTar;
}
/**
* @return the baseImageName
*/
public String getBaseImageName() {
return this.baseImageName;
}
/**
* Finds the name of the agent file.
* @param url the agent's URL (not null)
* @param isTar true if it is a TAR.GZ, false for a ZIP
* <p>
* This parameter is ignored unless the URL does not contain a valid file name.
* </p>
*
* @return a non-null string
*/
static String findAgentFileName( String url, boolean isTar ) {
String agentFilename = url.substring( url.lastIndexOf('/') + 1 );
if( url.toLowerCase().startsWith( "mvn:" )
|| agentFilename.contains( "?" )
|| agentFilename.contains( "&" ))
agentFilename = "roboconf-agent" + (isTar ? ".tar.gz" : ".zip");
return agentFilename;
}
/**
* Get the file name from a "file:" URL.
* @param url the URL of the file.
* @return the file name.
*/
static String getFileNameFromFileUrl( final String url ) {
String name = url.substring( url.lastIndexOf('/') + 1 );
int index = name.lastIndexOf( '?' );
if( index > 0 )
name = name.substring( 0, index );
else if( index == 0 )
name = name.substring( 1 );
return name.replaceAll( "[^\\w.-]", "_" );
}
/**
* @param bundleUrls a non-null list of URLs (relative to the Docker container)
* @return the content of a Karaf feature, or null if no feature has to be installed
* @throws IOException
*/
static String prepareKarafFeature( List<String> bundleUrls ) throws IOException {
String result = null;
if( ! bundleUrls.isEmpty()) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
InputStream in = DockerfileGenerator.class.getResourceAsStream( "/feature-tpl.xml" );
Utils.copyStreamSafely( in, os );
StringBuilder sb = new StringBuilder();
for( String url : bundleUrls ) {
sb.append( "<bundle>" );
sb.append( url );
sb.append( "</bundle>\n" );
}
result = os.toString( "UTF-8" ).replace( "%CONTENT%", sb.toString());
}
return result;
}
/**
* This methods prepares the actions to perform for agent extensions.
* @return a string to append to the dockerfile (never null)
* @throws IOException
* @throws URISyntaxException
*/
String handleAdditionalDeployments( File dockerFileLocation ) throws IOException, URISyntaxException {
StringBuilder sb = new StringBuilder();
// Run through the list...
for( final String deployUrl : this.deployList ) {
// Local, Maven or remote URL?
// "ADD" supports remotes URLs. So, we must copy other kinds.
File fileToCopy = null;
if( deployUrl.toLowerCase().startsWith( "file:/" )) {
fileToCopy = new File( UriUtils.urlToUri( deployUrl ));
} else if( deployUrl.toLowerCase().startsWith( "mvn:" )) {
this.logger.fine( "Resolving a Maven URL: " + deployUrl );
fileToCopy = this.mavenResolver.resolve( deployUrl );
}
// Where to place the file in the image?
// JAR file? We will generate a feature.xml file, so put it in "backup".
// Otherwise, directly put it in Karaf's deploy directory
boolean isJar = fileToCopy != null && fileToCopy.getName().toLowerCase().endsWith( ".jar" );
isJar |= deployUrl.toLowerCase().endsWith( ".jar" );
String dir = RBCF_DIR + (isJar ? BACKUP : DEPLOY);
// Add whatever is necessary in the dockerfile
String name;
if( fileToCopy != null ) {
this.logger.fine( "Copying " + deployUrl + "..." );
Utils.copyStream( fileToCopy, new File( dockerFileLocation, fileToCopy.getName()));
sb.append( "ADD " + fileToCopy.getName() + " " + dir );
name = fileToCopy.getName();
} else {
sb.append( "ADD " + deployUrl + " " + dir );
name = getFileNameFromFileUrl( deployUrl );
}
// What should be added to the generated feature?
// Well, we keep the JAR files.
if( isJar ) {
if( fileToCopy != null )
this.bundleUrls.add( "file://" + dir + name );
else
this.bundleUrls.add( deployUrl );
}
}
return sb.toString();
}
}