/**
* Copyright 2015-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 static net.roboconf.target.docker.internal.DockerHandler.ADDITIONAL_DEPLOY;
import static net.roboconf.target.docker.internal.DockerHandler.ADDITIONAL_PACKAGES;
import static net.roboconf.target.docker.internal.DockerHandler.AGENT_JRE_AND_PACKAGES;
import static net.roboconf.target.docker.internal.DockerHandler.AGENT_JRE_AND_PACKAGES_DEFAULT;
import static net.roboconf.target.docker.internal.DockerHandler.AGENT_PACKAGE_URL;
import static net.roboconf.target.docker.internal.DockerHandler.BASE_IMAGE;
import static net.roboconf.target.docker.internal.DockerHandler.DEFAULT_DOCKER_IMAGE_REGISTRY;
import static net.roboconf.target.docker.internal.DockerHandler.DOCKER_IMAGE_REGISTRY;
import static net.roboconf.target.docker.internal.DockerHandler.DOWNLOAD_BASE_IMAGE;
import static net.roboconf.target.docker.internal.DockerHandler.GENERATE_IMAGE;
import static net.roboconf.target.docker.internal.DockerHandler.IMAGE_ID;
import static net.roboconf.target.docker.internal.DockerHandler.OPTION_PREFIX_ENV;
import static net.roboconf.target.docker.internal.DockerHandler.OPTION_PREFIX_RUN;
import static net.roboconf.target.docker.internal.DockerHandler.RUN_EXEC;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.ops4j.pax.url.mvn.MavenResolver;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.PullImageCmd;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.core.command.BuildImageResultCallback;
import com.github.dockerjava.core.command.PullImageResultCallback;
import net.roboconf.core.model.beans.Instance;
import net.roboconf.core.utils.ManifestUtils;
import net.roboconf.core.utils.MavenUtils;
import net.roboconf.core.utils.Utils;
import net.roboconf.target.api.AbstractThreadedTargetHandler.MachineConfigurator;
import net.roboconf.target.api.TargetException;
/**
* @author Vincent Zurczak - Linagora
* @author Pierre Bourret - Université Joseph Fourier
*/
public class DockerMachineConfigurator implements MachineConfigurator {
static final String DEFAULT_IMG_NAME = "generated.by.roboconf";
DockerClient dockerClient;
private final Logger logger = Logger.getLogger( getClass().getName());
private final Instance scopedInstance;
private final Map<String,String> targetProperties;
private final Map<String,String> messagingConfiguration;
private final String machineId, scopedInstancePath, applicationName;
private MavenResolver mavenResolver;
/**
* Constructor.
* @param targetProperties the target properties (e.g. access key, secret key, etc.)
* @param messagingConfiguration the messaging configuration
* @param machineId the ID machine of the machine to configure
* @param applicationName the application name
* @param scopedInstancePath the path of the scoped/root instance
* @param scopedInstance the scoped instance
*/
public DockerMachineConfigurator(
Map<String,String> targetProperties,
Map<String,String> messagingConfiguration,
String machineId,
String scopedInstancePath,
String applicationName,
Instance scopedInstance ) {
this.targetProperties = targetProperties;
this.messagingConfiguration = messagingConfiguration;
this.applicationName = applicationName;
this.scopedInstancePath = scopedInstancePath;
this.machineId = machineId;
this.scopedInstance = scopedInstance;
}
/**
* @param mavenResolver the mavenResolver to set
*/
public void setMavenResolver( MavenResolver mavenResolver ) {
this.mavenResolver = mavenResolver;
}
@Override
public Instance getScopedInstance() {
return this.scopedInstance;
}
@Override
public void close() throws IOException {
if( this.dockerClient != null )
this.dockerClient.close();
}
@Override
public boolean configure() throws TargetException {
// Creating a container is almost immediate.
// And building an image with the REST API is blocking the thread until the creation is complete.
// So, this is not asynchronous configuration.
// Said differently, this method will be invoked only once!
this.dockerClient = DockerUtils.createDockerClient( this.targetProperties );
String imageId = this.targetProperties.get( IMAGE_ID );
String fixedImageId = fixImageId( imageId );
String generateAS = this.targetProperties.get( GENERATE_IMAGE );
boolean generate = Boolean.parseBoolean( generateAS );
Image img = DockerUtils.findImageByIdOrByTag( fixedImageId, this.dockerClient );
if( generate && img == null )
createImage( fixedImageId );
createContainer( fixedImageId );
return true;
}
/**
* Creates a container.
* @param imageId the image ID
* @throws TargetException if something went wrong
*/
void createContainer( String imageId ) throws TargetException {
// Get the command to pass to the container
this.logger.info( "Creating container " + this.machineId + " from image " + imageId );
List<String> args = DockerUtils.buildRunCommand(
this.targetProperties.get( RUN_EXEC ),
this.messagingConfiguration , this.applicationName, this.scopedInstancePath );
// Deal with the Docker run options
Map<String,String> options = new HashMap<> ();
for( Map.Entry<String,String> entry : this.targetProperties.entrySet()) {
if( entry.getKey().toLowerCase().startsWith( OPTION_PREFIX_RUN )) {
String key = entry.getKey().substring( OPTION_PREFIX_RUN.length());
options.put( key, entry.getValue());
}
}
// Deal with environment variables
List<String> env = new ArrayList<> ();
for( Map.Entry<String,String> entry : this.targetProperties.entrySet()) {
if( entry.getKey().toLowerCase().startsWith( OPTION_PREFIX_ENV )) {
String key = entry.getKey().substring( OPTION_PREFIX_ENV.length());
String value = entry.getValue();
value = value.replace( "<application-name>", this.applicationName );
value = value.replace( "<scoped-instance-path>", this.scopedInstancePath );
env.add( key + "=" + value );
}
}
// Execute...
try {
String containerName = this.scopedInstancePath + "_from_" + this.applicationName;
containerName = containerName.replaceFirst( "^/", "" ).replace( "/", "-" ).replaceAll( "\\s+", "_" );
// Prevent container names from being too long (see #480)
if( containerName.length() > 61 )
containerName = containerName.substring( 0, 61 );
CreateContainerCmd cmd = this.dockerClient.createContainerCmd( imageId )
.withName( containerName )
.withEnv( env )
.withCmd( args.toArray( new String[ args.size()]));
DockerUtils.configureOptions( options, cmd );
CreateContainerResponse container = cmd.exec();
// Log warnings, if any
if( container.getWarnings() != null
&& container.getWarnings().length > 0
&& this.logger.isLoggable( Level.FINE )) {
StringBuilder sb = new StringBuilder();
sb.append( "The following warnings have been found.\n" );
for( String s : container.getWarnings())
sb.append( s ).append( '\n' );
this.logger.fine( sb.toString().trim());
}
// And start the container
this.dockerClient.startContainerCmd( container.getId()).exec();
// We're done here!
this.logger.fine( "Container " + this.machineId + " was succesfully created as " + container.getId());
// We replace the machine ID in the instance.
// The configurator will be stopped anyway.
this.scopedInstance.data.put( Instance.MACHINE_ID, container.getId());
} catch( Exception e ) {
throw new TargetException( e );
}
}
/**
* Creates an image.
* @param imageId the image ID
* @throws TargetException if something went wrong
*/
void createImage( String imageId ) throws TargetException {
this.logger.fine( "Trying to create image " + imageId + " from a generated Dockerfile." );
// Find the agent's package URL
String agentPackageUrl = this.targetProperties.get( AGENT_PACKAGE_URL );
if( Utils.isEmptyOrWhitespaces( agentPackageUrl )) {
String bundleVersion = ManifestUtils.findBundleVersion();
String mavenVersion = ManifestUtils.findMavenVersion( bundleVersion );
if( bundleVersion == null )
throw new TargetException( "Roboconf's version could not be determined (guessing the agent package URL failed)." );
IOException exception = null;
try {
agentPackageUrl = MavenUtils.findMavenUrl( "net.roboconf", "roboconf-karaf-dist-agent", mavenVersion, "tar.gz" );
} catch( IOException e ) {
exception = e;
}
if( Utils.isEmptyOrWhitespaces( agentPackageUrl )) {
String s = "No Maven package was found for the agent distribution " + mavenVersion + " (guessing the agent package URL failed).";
throw new TargetException( s, exception );
}
}
// Verify the base image exists, if any
// (otherwise, a default one is provided by the image generator).
String baseImageRef = this.targetProperties.get( BASE_IMAGE );
if( ! Utils.isEmptyOrWhitespaces( baseImageRef )) {
Image baseImage = DockerUtils.findImageByIdOrByTag( baseImageRef, this.dockerClient );
String downloadImage = this.targetProperties.get( DOWNLOAD_BASE_IMAGE );
if( baseImage == null ) {
// Should we download it?
if( ! Boolean.parseBoolean( downloadImage ))
throw new TargetException( "Base image '" + baseImageRef + "' was not found. Image generation is not possible." );
// Download the base image then
String imageRegistry = Utils.getValue( this.targetProperties, DOCKER_IMAGE_REGISTRY, DEFAULT_DOCKER_IMAGE_REGISTRY );
this.logger.fine( "Asking Docker to download image '" + baseImageRef + "' from the registry: " + imageRegistry );
List<String> imageInfos = Utils.splitNicely( baseImageRef, ":" );
PullImageCmd cmd = this.dockerClient.pullImageCmd( imageInfos.get( 0 )).withRegistry( imageRegistry );
if( imageInfos.size() > 1 )
cmd = cmd.withTag( imageInfos.get( 1 ));
try {
cmd.exec( new RoboconfPullImageResultCallback()).awaitSuccess();
} catch( Exception e ) {
Utils.logException( this.logger, e );
throw new TargetException( "An error occurred while downloading image '" + baseImageRef + "'.", e );
}
baseImage = DockerUtils.findImageByIdOrByTag( baseImageRef, this.dockerClient);
if( baseImage == null)
throw new TargetException( "Base image '" + baseImageRef + "' was not found, even after a download attempt. Image generation is not possible." );
this.logger.info( "Base image '" + baseImageRef + "' was successfulyl downloaded." );
}
}
// Build the packages list.
String packages = this.targetProperties.get( AGENT_JRE_AND_PACKAGES );
final String additionalPackages = this.targetProperties.get( ADDITIONAL_PACKAGES );
if( ! Utils.isEmptyOrWhitespaces(additionalPackages)) {
// Sets to the default here, so we do not override the JRE package.
if (Utils.isEmptyOrWhitespaces(packages))
packages = AGENT_JRE_AND_PACKAGES_DEFAULT;
packages = packages + ' ' + additionalPackages;
}
// Build the additional URLs-to-deploy list.
final String deploy = this.targetProperties.get( ADDITIONAL_DEPLOY );
List<String> deployList;
if( Utils.isEmptyOrWhitespaces( deploy ))
deployList = Collections.emptyList();
else
deployList = Utils.splitNicely( deploy, " " );
// Create the image
File dockerfile = null;
try {
// Generate a Dockerfile
DockerfileGenerator gen = dockerfileGenerator( agentPackageUrl, packages, deployList, baseImageRef );
dockerfile = gen.generateDockerfile();
// Start the build.
// This will block the current thread until the creation is complete.
this.logger.fine( "Asking Docker to build the image from our Dockerfile." );
String builtImageId = this.dockerClient
.buildImageCmd( dockerfile )
.withTag( imageId )
.exec( new RoboconfBuildImageResultCallback())
.awaitImageId();
// No need to store the real image ID... Docker has it.
// Besides, we search images by both IDs and tags.
// Anyway, we can log the information anyway.
this.logger.fine( "Image '" + builtImageId + "' was succesfully generated by Roboconf." );
} catch( Exception e ) {
Utils.logException( this.logger, e );
throw new TargetException( e );
} finally {
Utils.deleteFilesRecursivelyAndQuietly( dockerfile );
}
}
/**
* @param baseImageRef
* @param deployList
* @param packages
* @param agentPackageUrl
* @return a new Docker image generator (externalized for tests)
*/
DockerfileGenerator dockerfileGenerator(String agentPackageUrl , String packages , List<String> deployList, String baseImageRef ) {
DockerfileGenerator gen = new DockerfileGenerator( agentPackageUrl, packages, deployList, baseImageRef );
gen.setMavenResolver( this.mavenResolver );
return gen;
}
/**
* @param imageId
* @return a non-null string
*/
static String fixImageId( String imageId ) {
return Utils.isEmptyOrWhitespaces( imageId ) ? DEFAULT_IMG_NAME : imageId;
}
/**
* A call back invoked when a Docker image was pulled.
* <p>
* No anonymous class in Roboconf.
* </p>
*
* @author Vincent Zurczak - Linagora
*/
static class RoboconfPullImageResultCallback extends PullImageResultCallback {
// nothing
}
/**
* A call back invoked when a Docker image was built.
* <p>
* No anonymous class in Roboconf.
* </p>
*
* @author Vincent Zurczak - Linagora
*/
static class RoboconfBuildImageResultCallback extends BuildImageResultCallback {
// nothing
}
}