/** * Copyright 2000-2014 NeuStar, Inc. All rights reserved. * NeuStar, the Neustar logo and related names and logos are registered * trademarks, service marks or tradenames of NeuStar, Inc. All other * product names, company names, marks, logos and symbols may be trademarks * of their respective owners. */ package biz.neustar.jenkins.plugins.packer; import hudson.AbortException; import hudson.CopyOnWrite; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Result; import hudson.model.TaskListener; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import hudson.util.ArgumentListBuilder; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.logging.Logger; /** * Publishing task that performs a call to a Packer executable * See: <a href="http://packer.io">Packer</a> * */ public class PackerPublisher extends Recorder { private static final Logger LOGGER = Logger.getLogger(PackerPublisher.class.getName()); public static final String TEMPLATE_MODE = "templateMode"; private final String name; private String jsonTemplate = ""; private String jsonTemplateText; private String packerHome = ""; private String params = ""; private final boolean useDebug; private final String changeDir; private String templateMode = TemplateMode.GLOBAL.toMode(); private List<PackerFileEntry> fileEntries = Collections.emptyList(); @DataBoundConstructor public PackerPublisher(String name, String jsonTemplate, String jsonTemplateText, String packerHome, String params, List<PackerFileEntry> fileEntries, boolean useDebug, String changeDir) { this.name = name; this.jsonTemplate = jsonTemplate; this.jsonTemplateText = jsonTemplateText; this.packerHome = packerHome; this.params = params; this.fileEntries = fileEntries; this.useDebug = useDebug; this.changeDir = changeDir; } public String getName() { return getInstallation().getName(); } public String getPackerHome() { return packerHome; } public void setPackerHome(String packerHome) { this.packerHome = packerHome; } public List<PackerFileEntry> getFileEntries() { if (fileEntries == null) { return Collections.emptyList(); } return fileEntries; } public void setFileEntries(List<PackerFileEntry> fileEntries) { this.fileEntries = fileEntries; } public String getJsonTemplate() { return jsonTemplate; } public void setJsonTemplate(String jsonTemplate) { this.jsonTemplate = jsonTemplate; } public String getJsonTemplateText() { return jsonTemplateText; } public void setJsonTemplateText(String jsonTemplateText) { this.jsonTemplateText = jsonTemplateText; } // This method is for output of user-friendly text only. public String getGlobalTemplate() { PackerInstallation installation = getInstallation(); if (installation.isTextTemplate()) { return installation.getJsonTemplateText(); } return "Using File: " + installation.getJsonTemplate(); } public static String createJsonTemplateTextTempFile(FilePath workspacePath, String contents) throws AbortException { try { LOGGER.info("jsonTemplateText: " + contents); if (Util.fixEmpty(contents) != null) { FilePath jsonFile = workspacePath.createTextTempFile("packer", ".json", contents, false); LOGGER.info("Using temp file: " + jsonFile.getRemote()); return jsonFile.getRemote(); } } catch (IOException ioe) { LOGGER.warning(convertException(ioe)); } catch (InterruptedException inte) { LOGGER.warning(convertException(inte)); } throw new AbortException("Template Generation / Loading Failed"); } public String createJsonTemplateTextTempFile(FilePath workspacePath) throws AbortException { return createJsonTemplateTextTempFile(workspacePath, jsonTemplateText); } public String getTemplateMode() { return templateMode; } // TODO: @DataBoundSetter public void setTemplateMode(String templateMode) { this.templateMode = templateMode; } public String getParams() { return params; } public void setParams(String params) { this.params = params; } public boolean getUseDebug() { return useDebug; } public String getChangeDir() { return this.changeDir; } public boolean isFileTemplate() { return TemplateMode.FILE.isMode(templateMode); } public boolean isTextTemplate() { return TemplateMode.TEXT.isMode(templateMode); } public boolean isGlobalTemplate() { return TemplateMode.GLOBAL.isMode(templateMode); } public boolean isGlobalTemplateChecked() { return isGlobalTemplate() || (!isFileTemplate() && !isTextTemplate()); } public PackerInstallation getInstallation() { for (PackerInstallation install : getDescriptor().getInstallations()) { if (name != null && install.getName().equals(name)) { return install; } } return null; } // in Windows packer installation has packer.exe is located in packer_home // Windows exec file is packer.exe (will refer to as "packer") public String getRemotePackerExec(AbstractBuild build, Launcher launcher, TaskListener listener) throws AbortException { String home = getPackerHome(); String remoteExec = null; if (Util.fixEmpty(home) == null) { PackerInstallation install = getInstallation(); try { install = install.forNode(build.getBuiltOn(), listener) .forEnvironment(build.getEnvironment(listener)); remoteExec = install.getExecutable(launcher); } catch (Exception ex) { LOGGER.severe(convertException(ex)); throw new AbortException("Tool Installation Failed for: " + getName()); } } else { FilePath execPath = getRemotePath(build, home); if (!home.toLowerCase().endsWith(PackerInstallation.WINDOWS_PACKER_COMMAND)) { execPath = new FilePath(execPath, isFilePathUnix(execPath) ? PackerInstallation.UNIX_PACKER_COMMAND : PackerInstallation.WINDOWS_PACKER_COMMAND); } remoteExec = execPath.getRemote(); } LOGGER.info("Using packer: " + remoteExec); return remoteExec; } public String getRemoteTemplate(AbstractBuild build, String... remotePaths) { FilePath templatePath = getRemotePath(build, remotePaths); LOGGER.info("Using templatePath: " + templatePath); return templatePath.getRemote(); } // either absolute or relative to project workspace public FilePath getRemotePath(AbstractBuild build, String... remotePaths) { FilePath result = build.getWorkspace(); for (String remotePath : remotePaths) { String path = remotePath.trim(); if (!path.isEmpty()) { result = new FilePath(result, path); } } return result; } /** * Create the temporary files from the configured entries. * @return the cmd line variable value for those entries. */ public String createTempFileEntries(FilePath workspacePath) throws AbortException { StringBuilder variables = new StringBuilder(); PackerInstallation install = getInstallation(); HashMap<String, PackerFileEntry> fileEntries = new HashMap<>(); if (install != null) { for (PackerFileEntry entry : install.getFileEntries()) { fileEntries.put(entry.getVarFileName(), entry); } } try { // potentially replace a global, which is what we want. for (PackerFileEntry entry : getFileEntries()) { fileEntries.put(entry.getVarFileName(), entry); } for (PackerFileEntry entry : fileEntries.values()) { // should be at least 1 character otherwise that shouldnt be allowed. String prefix = "packer-plugin-" + entry.getVarFileName(); FilePath entryFile = workspacePath.createTextTempFile(prefix, ".tmp", entry.getContents(), false); variables.append(String.format("-var \"%s=%s\" ", entry.getVarFileName(), entryFile.getRemote())); } } catch (IOException e) { LOGGER.severe(convertException(e)); throw new AbortException("File Entry Generation Failed"); } catch (InterruptedException e) { LOGGER.severe(convertException(e)); throw new AbortException("File Entry Generation Failed"); } return variables.toString(); } @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { ArgumentListBuilder args = new ArgumentListBuilder(); try { args.add(getRemotePackerExec(build, launcher, listener)).add("build"); EnvVars env = build.getEnvironment(listener); PackerInstallation installation = getInstallation(); // mask the global params. for (String param : addParamsAsArgs(Util.fixNull(installation.getParams()))) { String addParam = param.trim(); if (addParam.length() > 0) { args.add(Util.replaceMacro(addParam, env), true); } } for (String param : addParamsAsArgs(getParams())) { String addParam = param.trim(); if (addParam.length() > 0) { args.add(Util.replaceMacro(addParam, env)); } } for (String val : addParamsAsArgs(createTempFileEntries(build.getWorkspace()))) { args.add(val); } if (getUseDebug()) { args.add("-debug"); } FilePath workingDir = workingDir(build, env); LOGGER.info("using working dir: " + workingDir); if (isGlobalTemplate()) { LOGGER.info("Using GlobalTemplate"); if (installation.isFileTemplate()) { args.add(getRemoteTemplate(build, Util.replaceMacro(getChangeDir(), env), Util.replaceMacro(installation.getJsonTemplate(), env))); } else { args.add(createJsonTemplateTextTempFile(workingDir, installation.getJsonTemplateText())); } } else if (isTextTemplate()) { LOGGER.info("Using TextTemplate"); args.add(createJsonTemplateTextTempFile(workingDir)); } else if (isFileTemplate()) { LOGGER.info("Using FileTemplate"); args.add(getRemoteTemplate(build, Util.replaceMacro(getChangeDir(), env), Util.replaceMacro(getJsonTemplate(), env))); } else { // throw LOGGER.warning("Unknown Template"); throw new AbortException("Unknown Template / Loading Failed"); } try { LOGGER.info("launch: " + args.toString()); if (launcher.launch().pwd(workingDir).cmds(args).envs(env).stdout(listener).join() == 0) { listener.finished(Result.SUCCESS); // parse the log to look for the image id return true; } } catch (Exception ex) { LOGGER.severe(convertException(ex)); listener.fatalError("Execution failed: " + args); } } catch (Exception e) { LOGGER.severe(convertException(e)); listener.fatalError("Execution failed: " + args); } listener.finished(Result.FAILURE); return false; } protected FilePath workingDir(AbstractBuild build, EnvVars env) { if (Util.fixEmpty(getChangeDir()) != null) { return new FilePath(build.getWorkspace().getChannel(), Util.replaceMacro(getChangeDir(),env)); } return build.getWorkspace(); } protected static String convertException(Exception ex) { StringWriter stringWriter = new StringWriter(); ex.printStackTrace(new PrintWriter(stringWriter)); return stringWriter.toString(); } // Adapted from FilePath since this method is not public public static boolean isFilePathUnix(FilePath path) { if(!path.isRemote()) { return File.pathSeparatorChar != ';'; } String remote = path.getRemote(); // Windows absolute path is 'X:\...', so this is usually a good indication of Windows path if(remote.length() > 3 && remote.charAt(1) == ':' && remote.charAt(2) == '\\') { return false; } return remote.indexOf("\\") == -1; } public static List<String> addParamsAsArgs(String params) { List<String> args = new ArrayList<>(); if (params == null || params.isEmpty()) { return args; } char[] chars = params.toCharArray(); int captureIndex = -1; char quoteChar = '\0'; boolean inQuote = false; boolean stripQuotes = false; for (int index = 0; index < chars.length; index++) { char c = chars[index]; if (inQuote && c == quoteChar) { // finished inQuote = false; } else if (captureIndex > -1 && !inQuote && c == ' ') { int start = stripQuotes ? captureIndex + 1 : captureIndex; int stop = stripQuotes ? index - 1 : index; args.add(params.substring(start, stop)); captureIndex = -1; stripQuotes = false; } else if (captureIndex == -1 && !inQuote && c != ' ') { captureIndex = index; if (c == '\'' || c == '\"') { inQuote = true; quoteChar = c; stripQuotes = true; } } } if (captureIndex > -1) { int start = stripQuotes ? captureIndex + 1 : captureIndex; int stop = stripQuotes ? params.length() - 1 : params.length(); args.add(params.substring(start, stop)); } return args; } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> { @Override public PackerPublisher newInstance(StaplerRequest req, JSONObject formData) throws hudson.model.Descriptor.FormException { PackerPublisher packer = (PackerPublisher) req.bindJSON(clazz, formData); if (formData.has(TEMPLATE_MODE)) { JSONObject opt = formData.getJSONObject(TEMPLATE_MODE); packer.setTemplateMode(opt.getString("value")); packer.setJsonTemplate(opt.optString("jsonTemplate")); packer.setJsonTemplateText(opt.optString("jsonTemplateText")); } return packer; } @CopyOnWrite private volatile PackerInstallation[] installations = new PackerInstallation[0]; public PackerInstallation[] getInstallations() { return installations; } public void setInstallations(PackerInstallation... installations) { this.installations = installations; save(); } public boolean isGlobalTemplateChecked(PackerPublisher instance) { boolean result = true; if (instance != null) { result = instance.isGlobalTemplateChecked(); } return result; } public String getGlobalTemplate(PackerPublisher instance) { String result = "Save and reload to see global template..."; if (instance != null) { result = instance.getGlobalTemplate(); } return result; } /** * In order to load the persisted global configuration, you have to call * load() in the constructor. */ public DescriptorImpl() { load(); } public boolean isApplicable(Class<? extends AbstractProject> aClass) { return true; } public String getDisplayName() { return "Packer"; } } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } }