/* * Copyright 2013 Qubell, Inc. * * 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 com.qubell.jenkinsci.plugins.qubell.builders; import com.qubell.jenkinsci.plugins.qubell.Configuration; import com.qubell.jenkinsci.plugins.qubell.JsonParser; import com.qubell.services.*; import com.qubell.services.exceptions.*; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.Result; import hudson.util.FormValidation; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import javax.servlet.ServletException; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; import java.util.UUID; /** * Launches a Qubell instance and saves instance id. Has to be executed before {@link RunCommandBuilder} or {@link DestroyInstanceBuilder} * * @author Alex Krupnov */ public class StartInstanceBuilder extends QubellBuilder { private static String MANIFEST_TEMP_NAME = "project_manifest.yaml"; private final String manifestRelativePath; private String manifestRelativePathResolved; private final String environmentId; private String environmentIdResolved; private final String applicationId; private String applicationIdResolved; private final String extraParameters; private String extraParametersResolved; /** * Manifest file path, relative to project workspace * * @return value of path, exposed for Jelly */ public String getManifestRelativePath() { return manifestRelativePath; } /** * Qubell environment id, optional * * @return environment id */ public String getEnvironmentId() { return environmentId; } /** * Qubell application id to launch, required * * @return value of application id */ public String getApplicationId() { return applicationId; } /** * Extra parameters as JSON object string representation * * @return value of json string */ public String getExtraParameters() { return extraParameters; } /** * Data bound constructor, executed by Jenkins * * @param manifestRelativePath see {@link #getManifestRelativePath()} * @param timeout see {@link #getTimeout()} * @param environmentId see {@link #getEnvironmentId()} * @param applicationId see {@link #getApplicationId()} * @param extraParameters see {@link #getExtraParameters()} * @param outputFilePath path to output file * @param failureReaction a target build status which should be set when instnace returns failure status */ @DataBoundConstructor public StartInstanceBuilder(String manifestRelativePath, String timeout, String environmentId, String applicationId, String extraParameters, String outputFilePath, String failureReaction) { super(timeout, InstanceStatusCode.RUNNING, outputFilePath, failureReaction); this.manifestRelativePath = manifestRelativePath; this.environmentId = environmentId; this.applicationId = applicationId; this.extraParameters = extraParameters; } /** * Copies manifest from build workflow (either on master or slave), into temporary folder on master * * @param build current build * @param buildLog current build log * @return {@link FilePath} object for new temporary maifest file * @throws IOException when file could not be copied/accessed * @throws InterruptedException when operation is interrupted */ protected FilePath copyManifest(AbstractBuild build, PrintStream buildLog) throws IOException, InterruptedException { logMessage(buildLog, "copying manifest from current build workspace. Relative path is %s", manifestRelativePathResolved); FilePath destinationManifest = getTemporaryManifestPath(build, buildLog); FilePath sourceManifest = build.getWorkspace().child(manifestRelativePathResolved); if (!sourceManifest.exists()) { logMessage(buildLog, "Unable to find manifest file with relative path %s, target file %s does not exist\n", manifestRelativePathResolved, sourceManifest.toURI()); throw new FileNotFoundException("Manifest not found"); } //logMessage(buildLog, "Copying manifest running on %s at %s to %s", currentMachine, sourceManifest.toURI(), destinationManifest.toURI()); sourceManifest.copyTo(destinationManifest); return destinationManifest; } private FilePath getTemporaryManifestPath(AbstractBuild build, PrintStream buildLog) { FilePath masterWorkspaceRoot = getMasterWorkspaceRoot(build, buildLog); //Using base file name + guid to ensure there are no conflicts in manifest return masterWorkspaceRoot.child(UUID.randomUUID().toString() + "." + MANIFEST_TEMP_NAME); } /** * Performs a build with following steps * <ol> * <li>Copies manifest into temp folder</li> * <li>Updates manifest for qubell application</li> * <li>Launches application instance</li> * <li>Waits when instance turned into Running state</li> * <li>Saves instance id for {@link RunCommandBuilder} or {@link DestroyInstanceBuilder}</li> * </ol> * * @param build current build * @param launcher build launcher * @param listener listener * @return true of builder did not fail, otherwise false * @throws IOException * @throws InterruptedException */ @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { resolveParameterPlaceholders(build, listener); FilePath manifestFile; Manifest manifest; PrintStream buildLog = listener.getLogger(); if (!validateConfiguration()) { logMessage(buildLog, "Unable to proceed without configuration. Please check global settings page."); build.setResult(Result.FAILURE); return false; } Application application = new Application(applicationIdResolved); Integer updatedVersion = 0; if (!StringUtils.isBlank(manifestRelativePathResolved)) { try { manifestFile = copyManifest(build, buildLog); manifest = new Manifest(manifestFile.readToString()); logMessage(buildLog, "Deleting temporary manifestFile"); manifestFile.delete(); } catch (FileNotFoundException fnfe) { logMessage(buildLog, "Unable to proceed without manifest"); build.setResult(Result.FAILURE); return false; } catch (IOException ioe) { logMessage(buildLog, "Unable to read manifest file"); build.setResult(Result.FAILURE); return false; } logMessage(buildLog, "Updating app manifest"); try { updatedVersion = getServiceFacade().updateManifest(application, manifest); logMessage(buildLog, "Manifest updated. New version is %s", updatedVersion.toString()); } catch (QubellServiceException e) { logMessage(buildLog, "Error when updating manifest: %s", e.getMessage()); build.setResult(Result.FAILURE); return false; } } Instance instance; try { instance = getServiceFacade().launchInstance(new InstanceSpecification(application, updatedVersion), new LaunchSettings(new Environment(environmentIdResolved), JsonParser.parseMap(extraParametersResolved))); logMessage(buildLog, "Launched instance %s", instance.getId()); saveBuildVariable(build, INSTANCE_ID_KEY, instance.getId(), buildLog); } catch (QubellServiceException e) { logMessage(buildLog, "Error when launching instance: %s", e.getMessage()); build.setResult(Result.FAILURE); return false; } return waitForExpectedStatus(build, buildLog, instance); } @Override protected void resolveParameterPlaceholders(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException { super.resolveParameterPlaceholders(build, listener); this.manifestRelativePathResolved = resolveVariableMacros(build, listener, this.manifestRelativePath); this.environmentIdResolved = resolveVariableMacros(build, listener, this.environmentId); this.applicationIdResolved = resolveVariableMacros(build, listener, this.applicationId); this.extraParametersResolved = resolveVariableMacros(build, listener, this.extraParameters); } /** * Descriptor for {@link StartInstanceBuilder}. Used as a singleton. * The class is marked as public so that it can be accessed from views. * See <tt>src/main/resources/com/qubell/jenkinsci/plugins/qubell/builders/StartInstanceBuilder/*.jelly</tt> * for the actual HTML fragment for the configuration screen. */ @Extension // This indicates to Jenkins that this is an implementation of an extension point. public static final class StartInstanceDescriptor extends BaseDescriptor { /** * Performs on-the-fly validation of the form field application id * * @param value This parameter receives the value that the user has typed. * @return Indicates the outcome of the validation. This is sent to the browser. */ public FormValidation doCheckApplicationId(@QueryParameter String value) throws IOException, ServletException { try { new QubellFacadeImpl(Configuration.get()).getAllApplications(); if (value.length() == 0) return FormValidation.error("Please specify an application id"); return FormValidation.ok(); } catch (QubellServiceException qce) { return FormValidation.error(qce.getMessage()); } } /** * Performs on-the-fly validation of the form field environment id * * @param value This parameter receives the value that the user has typed. * @return Indicates the outcome of the validation. This is sent to the browser. */ public FormValidation doCheckEnvironmentId(@QueryParameter String value) throws IOException, ServletException { try { new QubellFacadeImpl(Configuration.get()).getAllEnvironments(); return FormValidation.ok(); } catch (QubellServiceException qce) { return FormValidation.error(qce.getMessage()); } } /** * Gets application list json for typeahead functionality * * @return json object for apps list */ public String getApplicationsTypeAheadJson() { try { return JsonParser.serialize(new QubellFacadeImpl(Configuration.get()).getAllApplications()); } catch (QubellServiceException qce) { // lets just silently swallow exception and hope that somebody is reading validation errors return "{}"; } } /** * Gets environments list json for typeahead functionality * * @return json object for envs list */ public String getEnvironmentsTypeAheadJson() { try { return JsonParser.serialize(new QubellFacadeImpl(Configuration.get()).getAllEnvironments()); } catch (QubellServiceException qce) { // lets just silently swallow exception and hope that somebody is reading validation errors return "{}"; } } /** * This human readable name is used in the configuration screen. */ public String getDisplayName() { return "Qubell: Launch Application Instance"; } } }