diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..0cbabc2 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,2 @@ +// Build on ci.jenkins.io; see https://github.com/jenkins-infra/pipeline-library +buildPlugin() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9123a6d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Byclosure + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/pom.xml b/pom.xml index b599c88..27ad822 100644 --- a/pom.xml +++ b/pom.xml @@ -1,62 +1,85 @@ - + 4.0.0 org.jenkins-ci.plugins plugin - 1.580.1 - + 2.7 + + com.byclosure.jenkins.plugins gcloud-sdk - 1.0-SNAPSHOT + 0.0.4-SNAPSHOT hpi + + 2.60.3 + 8 + none + + GCloud SDK Plugin - GCloud SDK Plugin allows the invocation of the gcloud CLI as a job step. - https://wiki.jenkins-ci.org/display/JENKINS/TODO+Plugin + GCloud SDK Plugin allows users to invoke gcloud tools with jenkins credentials. + https://wiki.jenkins-ci.org/display/JENKINS/GCloud+SDK+Plugin MIT License http://opensource.org/licenses/MIT - - + + + + scm:git:git://github.com/jenkinsci/gcloud-sdk-plugin.git + scm:git:git@github.com:jenkinsci/gcloud-sdk-plugin.git + https://github.com/jenkinsci/gcloud-sdk-plugin + HEAD + + repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ + https://repo.jenkins-ci.org/public/ + repo.jenkins-ci.org - http://repo.jenkins-ci.org/public/ + https://repo.jenkins-ci.org/public/ + + org.kohsuke + access-modifier-suppressions + 1.16 + compile + + + com.google.http-client + google-http-client-jackson2 + 1.24.1 + org.jenkins-ci.plugins google-oauth-plugin - 0.3 + 0.8 + + + org.jenkins-ci.plugins + structs + 1.7 diff --git a/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudBuildWrapper.java b/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudBuildWrapper.java index 96445cf..7ffc866 100644 --- a/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudBuildWrapper.java +++ b/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudBuildWrapper.java @@ -1,61 +1,109 @@ package com.byclosure.jenkins.plugins.gcloud; +import com.cloudbees.jenkins.plugins.gcloudsdk.GCloudInstallation; +import com.cloudbees.plugins.credentials.CredentialsMatcher; +import com.cloudbees.plugins.credentials.CredentialsMatchers; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; import com.google.jenkins.plugins.credentials.domains.RequiresDomain; +import com.google.jenkins.plugins.credentials.oauth.GoogleRobotPrivateKeyCredentials; +import hudson.EnvVars; import hudson.Extension; +import hudson.FilePath; import hudson.Launcher; -import hudson.model.AbstractBuild; import hudson.model.AbstractProject; -import hudson.model.BuildListener; -import hudson.tasks.BuildWrapper; +import hudson.model.Computer; +import hudson.model.Item; +import hudson.model.Node; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.security.ACL; import hudson.tasks.BuildWrapperDescriptor; +import hudson.util.ListBoxModel; +import jenkins.tasks.SimpleBuildWrapper; import net.sf.json.JSONObject; +import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.CheckForNull; import java.io.IOException; import java.util.Map; import java.util.logging.Logger; @RequiresDomain(value = GCloudScopeRequirement.class) -public class GCloudBuildWrapper extends BuildWrapper { +public class GCloudBuildWrapper extends SimpleBuildWrapper { private static final Logger LOGGER = Logger.getLogger(GCloudBuildWrapper.class.getName()); + + private final String installation; private final String credentialsId; @DataBoundConstructor - public GCloudBuildWrapper(String credentialsId) { - this.credentialsId = credentialsId; - } + public GCloudBuildWrapper(String installation, String credentialsId) { + this.installation = installation; + this.credentialsId = credentialsId; + } - @Override - public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { - final GCloudServiceAccount serviceAccount = - GCloudServiceAccount.getServiceAccount(build, launcher, listener, credentialsId); + @Override + public void setUp(Context context, Run build, FilePath workspace, Launcher launcher, TaskListener listener, EnvVars initialEnvironment) throws IOException, InterruptedException { - if (!serviceAccount.activate()) { - serviceAccount.cleanUp(); - throw new InterruptedException("Couldn't activate GCloudServiceAccount"); - } + GCloudInstallation sdk = getSDK(); + + if (sdk == null) { + throw new RuntimeException("Could not find a matching Google Cloud SDK installation: " + installation); + } + + Computer computer = workspace.toComputer(); + if (computer == null) throw new RuntimeException("Unable to get workspace node."); + Node node = computer.getNode(); + if (node == null) throw new RuntimeException("Unable to get workspace node."); - return new Environment() { - @Override - public void buildEnvVars(Map env) { - } + sdk = sdk.translate(node, initialEnvironment, listener); + final FilePath configDir = workspace.createTempDir("gcloud", "config"); - @Override - public boolean tearDown(AbstractBuild build, final BuildListener listener) throws IOException, InterruptedException { - if (!serviceAccount.revoke()) { - serviceAccount.cleanUp(); - return false; - } + final GCloudServiceAccount serviceAccount = + GCloudServiceAccount.getServiceAccount(build, launcher, listener, credentialsId, configDir); - serviceAccount.cleanUp(); - return true; - } + if (serviceAccount != null) { + if (!serviceAccount.activate(sdk)) { + configDir.deleteRecursive(); + throw new InterruptedException("Couldn't activate GCloudServiceAccount"); + } - }; + context.env("GOOGLE_APPLICATION_CREDENTIALS", serviceAccount.getKeyFile().getRemote()); + } + + context.env("CLOUDSDK_CONFIG", configDir.getRemote()); + + sdk.buildEnvVars(initialEnvironment); + for (Map.Entry entry : initialEnvironment.entrySet()) { + context.env(entry.getKey(), entry.getValue()); + } + + + context.setDisposer(new GCloudConfigDisposer(configDir)); } - public String getCredentialsId() { + public @CheckForNull GCloudInstallation getSDK() { + GCloudInstallation[] installations = GCloudInstallation.getInstallations(); + + if (installation.isEmpty() && installations.length > 0) { + return installations[0]; + } + + for (GCloudInstallation sdk : installations) { + if (installation.equals(sdk.getName())) return sdk; + } + return null; + } + + public String getInstallation() { + return installation; + } + + public String getCredentialsId() { return credentialsId; } @@ -67,7 +115,7 @@ public DescriptorImpl() { @Override public String getDisplayName() { - return "GCloud authentication"; + return "GCloud SDK authentication"; } @Override @@ -80,5 +128,38 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc save(); return super.configure(req, formData); } + + public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item project, + @QueryParameter String serverAddress) { + if (project == null || !project.hasPermission(Item.CONFIGURE)) { + return new StandardListBoxModel(); + } + return new StandardListBoxModel() + .withEmptySelection() + .withMatching( + MATCHER, + CredentialsProvider.lookupCredentials(GoogleRobotPrivateKeyCredentials.class, + project, + ACL.SYSTEM, + URIRequirementBuilder.fromUri(serverAddress).build())); + } + + public static final CredentialsMatcher MATCHER = CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(GoogleRobotPrivateKeyCredentials.class)); } + + private static class GCloudConfigDisposer extends Disposer { + + private static final long serialVersionUID = 5723296082223871496L; + + private final FilePath configDir; + + public GCloudConfigDisposer(FilePath configDir) { + this.configDir = configDir; + } + + @Override + public void tearDown(Run build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { + configDir.deleteRecursive(); + } + } } diff --git a/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudSDKBuilder.java b/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudSDKBuilder.java index 7522330..d6215db 100644 --- a/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudSDKBuilder.java +++ b/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudSDKBuilder.java @@ -2,6 +2,7 @@ import com.google.jenkins.plugins.credentials.domains.RequiresDomain; +import hudson.EnvVars; import hudson.Extension; import hudson.Launcher; import hudson.model.AbstractBuild; @@ -17,9 +18,11 @@ import javax.servlet.ServletException; import java.io.IOException; +import java.util.logging.Logger; @RequiresDomain(value = GCloudScopeRequirement.class) public class GCloudSDKBuilder extends Builder { + private static final Logger LOGGER = Logger.getLogger(GCloudSDKBuilder.class.getName()); private final String command; @@ -42,9 +45,13 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListener lis } private boolean executeGCloudCLI(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { + EnvVars env = build.getEnvironment(listener); + String binariesPath = env.get("CLOUDSDK_DIR")+"/bin/"; + int retCode = launcher.launch() .pwd(build.getWorkspace()) - .cmdAsSingleString("gcloud " + command) + .cmdAsSingleString(binariesPath + "gcloud " + command) + .envs(env) .stdout(listener.getLogger()) .join(); diff --git a/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudSDKWithAuthBuilder.java b/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudSDKWithAuthBuilder.java index 3c32d87..4e5122c 100644 --- a/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudSDKWithAuthBuilder.java +++ b/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudSDKWithAuthBuilder.java @@ -3,6 +3,7 @@ import com.google.jenkins.plugins.credentials.domains.RequiresDomain; import hudson.Extension; +import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; @@ -30,46 +31,51 @@ public GCloudSDKWithAuthBuilder(String credentialsId, String command) { this.command = command; } + public String getCredentialsId() { + return credentialsId; + } + + public String getCommand() { return command; } @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + final FilePath ws = build.getWorkspace(); + if (ws == null) throw new RuntimeException("Unable to get build workspace."); + + final FilePath configDir = ws.createTempDir("gcloud", "config"); final GCloudServiceAccount serviceAccount = - GCloudServiceAccount.getServiceAccount(build, launcher, listener, credentialsId); + GCloudServiceAccount.getServiceAccount(build, launcher, listener, credentialsId, configDir); - if (!serviceAccount.activate()) { - serviceAccount.cleanUp(); - return false; - } + try { + if (serviceAccount == null) { + return false; + } - if (!executeGCloudCLI(build, launcher, listener)) { - serviceAccount.revoke(); - serviceAccount.cleanUp(); - return false; - } + if (!serviceAccount.activate(null)) { + return false; + } - if (!serviceAccount.revoke()) { - serviceAccount.cleanUp(); - return false; + if (!executeGCloudCLI(build, launcher, listener, configDir)) { + return false; + } + return true; + } finally { + configDir.deleteRecursive(); } - - serviceAccount.cleanUp(); - return true; } - private boolean executeGCloudCLI(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { + private boolean executeGCloudCLI(AbstractBuild build, Launcher launcher, BuildListener listener, FilePath configDir) throws IOException, InterruptedException { int retCode = launcher.launch() .pwd(build.getWorkspace()) .cmdAsSingleString("gcloud " + command) .stdout(listener.getLogger()) - .join(); + .envs("CLOUDSDK_CONFIG=" + configDir.getRemote()) + .join(); - if (retCode != 0) { - return false; - } - return true; + return retCode == 0; } @Override @@ -77,10 +83,6 @@ public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } - public String getCredentialsId() { - return credentialsId; - } - @Extension public static final class DescriptorImpl extends BuildStepDescriptor { public DescriptorImpl() { diff --git a/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudServiceAccount.java b/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudServiceAccount.java index 6de977e..6f74348 100644 --- a/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudServiceAccount.java +++ b/src/main/java/com/byclosure/jenkins/plugins/gcloud/GCloudServiceAccount.java @@ -1,27 +1,29 @@ package com.byclosure.jenkins.plugins.gcloud; +import com.cloudbees.jenkins.plugins.gcloudsdk.GCloudInstallation; import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.google.jenkins.plugins.credentials.oauth.GoogleRobotPrivateKeyCredentials; -import com.google.jenkins.plugins.credentials.oauth.JsonServiceAccountConfig; -import com.google.jenkins.plugins.credentials.oauth.P12ServiceAccountConfig; -import com.google.jenkins.plugins.credentials.oauth.ServiceAccountConfig; +import com.cloudbees.plugins.credentials.SecretBytes; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.jenkins.plugins.credentials.oauth.*; +import hudson.FilePath; import hudson.Launcher; -import hudson.model.AbstractBuild; -import hudson.model.BuildListener; +import hudson.model.Run; +import hudson.model.TaskListener; +import org.kohsuke.accmod.restrictions.suppressions.SuppressRestrictedWarnings; -import java.io.File; +import javax.annotation.Nullable; +import java.io.ByteArrayInputStream; import java.io.IOException; public class GCloudServiceAccount { - - private final AbstractBuild build; private final Launcher launcher; - private final BuildListener listener; + private final TaskListener listener; private final String accountId; private final TemporaryKeyFile tmpKeyFile; + private final FilePath configDir; - public static GCloudServiceAccount getServiceAccount(AbstractBuild build, Launcher launcher, - BuildListener listener, String credentialsId) throws IOException, InterruptedException { + public static GCloudServiceAccount getServiceAccount(Run build, Launcher launcher, + TaskListener listener, String credentialsId, FilePath configDir) throws IOException, InterruptedException { final GoogleRobotPrivateKeyCredentials credential = CredentialsProvider.findCredentialById( credentialsId, GoogleRobotPrivateKeyCredentials.class, @@ -29,60 +31,64 @@ public static GCloudServiceAccount getServiceAccount(AbstractBuild build, Launch new GCloudScopeRequirement() ); + if (credential == null) { + return null; + } + final ServiceAccountConfig serviceAccountConfig = credential.getServiceAccountConfig(); final String accountId = serviceAccountConfig.getAccountId(); - final File keyFile = getKeyFile(serviceAccountConfig); - - TemporaryKeyFile tmpKeyFile = new TemporaryKeyFile(build, launcher, keyFile); - tmpKeyFile.copyToTmpDir(); + final TemporaryKeyFile tmpKeyFile = getKeyFile(serviceAccountConfig, configDir); - return new GCloudServiceAccount(build, launcher, listener, accountId, tmpKeyFile); + return new GCloudServiceAccount(launcher, listener, accountId, tmpKeyFile, configDir); } - private static File getKeyFile(ServiceAccountConfig serviceAccount) { - String keyFilePath = null; - - if (serviceAccount instanceof JsonServiceAccountConfig) { - keyFilePath = ((JsonServiceAccountConfig)serviceAccount).getJsonKeyFile(); - } else if (serviceAccount instanceof JsonServiceAccountConfig) { - keyFilePath = ((P12ServiceAccountConfig)serviceAccount).getP12KeyFile(); + @SuppressRestrictedWarnings(JsonServiceAccountConfig.class) + @Nullable + private static TemporaryKeyFile getKeyFile(ServiceAccountConfig serviceAccount, FilePath configDir) { + TemporaryKeyFile tmpKeyFile = null; + + try { + if (serviceAccount instanceof JsonServiceAccountConfig) { + SecretBytes secretKey = ((JsonServiceAccountConfig) serviceAccount).getSecretJsonKey(); + + if (secretKey != null) { + JsonKey key = JsonKey.load(new JacksonFactory(), new ByteArrayInputStream(secretKey.getPlainData())); + tmpKeyFile = new TemporaryKeyFile(configDir, key.toPrettyString()); + } + } else if (serviceAccount instanceof P12ServiceAccountConfig) { + String keyFilePath = ((P12ServiceAccountConfig)serviceAccount).getP12KeyFile(); + tmpKeyFile = new TemporaryKeyFile(configDir, keyFilePath); + } + } catch (IOException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); } - return new File(keyFilePath); + return tmpKeyFile; } - private GCloudServiceAccount(AbstractBuild build, Launcher launcher, BuildListener listener, String accountId, TemporaryKeyFile tmpKeyFile) { - this.build = build; + private GCloudServiceAccount(Launcher launcher, TaskListener listener, String accountId, TemporaryKeyFile tmpKeyFile, FilePath configDir) { this.launcher = launcher; this.listener = listener; this.accountId = accountId; this.tmpKeyFile = tmpKeyFile; + this.configDir = configDir; } - boolean activate() throws IOException, InterruptedException { - final String authCmd = "gcloud auth activate-service-account " + accountId + " --key-file " + tmpKeyFile.getKeyFile().getRemote(); - - int retCode = launcher.launch() - .pwd(build.getWorkspace()) - .cmdAsSingleString(authCmd) - .stdout(listener.getLogger()) - .join(); - - if (retCode != 0) { - return false; + boolean activate(GCloudInstallation sdk) throws IOException, InterruptedException { + String exec = "gcloud"; + if (sdk != null) { + exec = sdk.getExecutable(); } - return true; - } - - boolean revoke() throws IOException, InterruptedException { - final String revokeCmd = "gcloud auth revoke " + accountId; + final String authCmd = exec + " auth activate-service-account " + accountId + " --key-file \"" + tmpKeyFile.getKeyFile().getRemote() + "\""; int retCode = launcher.launch() - .pwd(build.getWorkspace()) - .cmdAsSingleString(revokeCmd) - .stdout(listener.getLogger()) - .join(); + .cmdAsSingleString(authCmd) + .stdout(listener.getLogger()) + .envs("CLOUDSDK_CONFIG=" + configDir.getRemote()) + .join(); if (retCode != 0) { return false; @@ -90,8 +96,7 @@ boolean revoke() throws IOException, InterruptedException { return true; } - - void cleanUp() throws IOException, InterruptedException { - tmpKeyFile.remove(); + FilePath getKeyFile(){ + return tmpKeyFile.getKeyFile(); } } \ No newline at end of file diff --git a/src/main/java/com/byclosure/jenkins/plugins/gcloud/TemporaryKeyFile.java b/src/main/java/com/byclosure/jenkins/plugins/gcloud/TemporaryKeyFile.java index d702d27..511395e 100644 --- a/src/main/java/com/byclosure/jenkins/plugins/gcloud/TemporaryKeyFile.java +++ b/src/main/java/com/byclosure/jenkins/plugins/gcloud/TemporaryKeyFile.java @@ -1,42 +1,23 @@ package com.byclosure.jenkins.plugins.gcloud; import hudson.FilePath; -import hudson.Launcher; -import hudson.model.AbstractBuild; -import java.io.File; import java.io.IOException; class TemporaryKeyFile { - private AbstractBuild build; - private Launcher launcher; - private File keyFile; - private FilePath tmpDir; private FilePath tmpKeyFile; - public TemporaryKeyFile(AbstractBuild build, Launcher launcher, File keyFile) { - this.build = build; - this.launcher = launcher; - this.keyFile = keyFile; + public TemporaryKeyFile(FilePath configDir, String content) throws IOException, InterruptedException { + tmpKeyFile = configDir.createTempFile("gcloud", "key"); + tmpKeyFile.write(content, "UTF-8"); } - public FilePath getDir() { - return tmpDir; + public TemporaryKeyFile(FilePath configDir, FilePath file) throws IOException, InterruptedException { + tmpKeyFile = configDir.createTempFile("gcloud", "key"); + tmpKeyFile.copyFrom(file); } public FilePath getKeyFile() { return tmpKeyFile; } - - public TemporaryKeyFile copyToTmpDir() throws IOException, InterruptedException { - tmpDir = build.getWorkspace().createTempDir("gcloud", null); - tmpKeyFile = new FilePath(launcher.getChannel(), - new File(tmpDir.getRemote(), keyFile.getName()).getPath()); - tmpKeyFile.copyFrom(new FilePath(keyFile)); - return this; - } - - void remove() throws IOException, InterruptedException { - tmpDir.deleteRecursive(); - } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/gcloudsdk/GCloudInstallation.java b/src/main/java/com/cloudbees/jenkins/plugins/gcloudsdk/GCloudInstallation.java new file mode 100644 index 0000000..1b9151b --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/gcloudsdk/GCloudInstallation.java @@ -0,0 +1,78 @@ +package com.cloudbees.jenkins.plugins.gcloudsdk; + +import hudson.EnvVars; +import hudson.Extension; +import hudson.model.EnvironmentSpecific; +import hudson.model.Node; +import hudson.model.TaskListener; +import hudson.slaves.NodeSpecific; +import hudson.tools.ToolDescriptor; +import hudson.tools.ToolInstallation; +import hudson.tools.ToolProperty; +import jenkins.model.Jenkins; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.io.IOException; +import java.util.List; + +/** + * @author Nicolas De Loof + */ +public class GCloudInstallation extends ToolInstallation implements NodeSpecific, EnvironmentSpecific { + + @DataBoundConstructor + public GCloudInstallation(String name, String home, List> properties) { + super(name, home, properties); + } + + + public void buildEnvVars(EnvVars env) { + env.override("PATH+GCLOUD", getHome()+"/bin"); + env.override("CLOUDSDK_DIR", getHome()); + env.override("CLOUDSDK_PYTHON_SITEPACKAGES", "1"); + } + + + @Override + public GCloudInstallation translate(Node node, EnvVars envs, TaskListener listener) throws IOException, InterruptedException { + return (GCloudInstallation) super.translate(node, envs, listener); + } + + @Override + public GCloudInstallation forNode(Node node, TaskListener log) throws IOException, InterruptedException { + return new GCloudInstallation(getName(), translateFor(node, log), getProperties().toList()); + } + + @Override + public GCloudInstallation forEnvironment(EnvVars environment) { + return new GCloudInstallation(getName(), environment.expand(getHome()), getProperties().toList()); + } + + public static GCloudInstallation[] getInstallations() { + return Jenkins.getInstance().getDescriptorByType(DescriptorImpl.class).getInstallations(); + } + + public String getExecutable() { + return getHome()+"/bin/gcloud"; + } + + @Extension @Symbol("gcloud") + public static class DescriptorImpl extends ToolDescriptor { + + @Override + public String getDisplayName() { + return "Google Cloud SDK"; + } + + public DescriptorImpl() { + load(); + } + + public void setInstallations(GCloudInstallation... installations) { + super.setInstallations(installations); + save(); + } + + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/gcloudsdk/GCloudInstaller.java b/src/main/java/com/cloudbees/jenkins/plugins/gcloudsdk/GCloudInstaller.java new file mode 100644 index 0000000..70b610d --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/gcloudsdk/GCloudInstaller.java @@ -0,0 +1,86 @@ +package com.cloudbees.jenkins.plugins.gcloudsdk; + +import hudson.Extension; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.Node; +import hudson.model.TaskListener; +import hudson.tools.DownloadFromUrlInstaller; +import hudson.tools.ToolInstallation; +import hudson.util.ArgumentListBuilder; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +/** + * @author Nicolas De Loof + */ +public class GCloudInstaller extends DownloadFromUrlInstaller { + + + private String additionalComponents; + + @DataBoundConstructor + public GCloudInstaller(String id, String additionalComponents) { + super(id); + this.additionalComponents = additionalComponents; + } + + public String getAdditionalComponents() { + return additionalComponents; + } + + @Override + public FilePath performInstallation(ToolInstallation tool, Node node, TaskListener log) throws IOException, InterruptedException { + FilePath installation = super.performInstallation(tool, node, log); + + Launcher launcher = node.createLauncher(log); + + ArgumentListBuilder args = new ArgumentListBuilder(); + args.add(installation.child(launcher.isUnix() ? "install.sh" : "install.bat").getRemote()) + .add("--usage-reporting=false", "--path-update=false", "--bash-completion=false"); + if (StringUtils.isNotBlank(additionalComponents)) + args.add("--additional-components", additionalComponents); + + launcher.launch() + .stdout(log) + .cmds(args.toCommandArray()) + .join(); + + return installation; + } + + @Override + public Installable getInstallable() throws IOException { + return SDK; + } + + @Extension + public static final class DescriptorImpl extends DownloadFromUrlInstaller.DescriptorImpl { + public String getDisplayName() { + return "Install from google.com"; + } + + @Override + public List getInstallables() throws IOException { + return Collections.singletonList(SDK); + } + + @Override + public boolean isApplicable(Class toolType) { + return GCloudInstallation.class.isAssignableFrom(toolType); + } + } + + + public static final Installable SDK = new Installable() { + { + id = "google-cloud-sdk"; + url = "https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.zip"; + name = "latest"; + } + }; +} diff --git a/src/main/resources/com/byclosure/jenkins/plugins/gcloud/GCloudBuildWrapper/config.jelly b/src/main/resources/com/byclosure/jenkins/plugins/gcloud/GCloudBuildWrapper/config.jelly index b6f26ea..ca98dc4 100644 --- a/src/main/resources/com/byclosure/jenkins/plugins/gcloud/GCloudBuildWrapper/config.jelly +++ b/src/main/resources/com/byclosure/jenkins/plugins/gcloud/GCloudBuildWrapper/config.jelly @@ -1,5 +1,9 @@ - + xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:a="/lib/auth" xmlns:g="/lib/gcloud" xmlns:c="/lib/credentials"> + + + + + diff --git a/src/main/resources/com/byclosure/jenkins/plugins/gcloud/GCloudSDKBuilder/config.jelly b/src/main/resources/com/byclosure/jenkins/plugins/gcloud/GCloudSDKBuilder/config.jelly index dd261a7..1db5265 100644 --- a/src/main/resources/com/byclosure/jenkins/plugins/gcloud/GCloudSDKBuilder/config.jelly +++ b/src/main/resources/com/byclosure/jenkins/plugins/gcloud/GCloudSDKBuilder/config.jelly @@ -1,6 +1,7 @@ + xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:a="/lib/auth" > + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/gcloudsdk/GCloudInstaller/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/gcloudsdk/GCloudInstaller/config.jelly new file mode 100644 index 0000000..5c4c52a --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/gcloudsdk/GCloudInstaller/config.jelly @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/lib/gcloud/selectGCloudInstallation.jelly b/src/main/resources/lib/gcloud/selectGCloudInstallation.jelly new file mode 100644 index 0000000..e76bf66 --- /dev/null +++ b/src/main/resources/lib/gcloud/selectGCloudInstallation.jelly @@ -0,0 +1,23 @@ + + + + + Renders a form field to select a GCloud installation. + The field is expected to be a String tool name, with Util.fixEmpty on the @DataBoundSetter to account for the default. + + Form field name. Used for databinding. + + + + + + + + + + diff --git a/src/main/resources/lib/gcloud/taglib b/src/main/resources/lib/gcloud/taglib new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/main/resources/lib/gcloud/taglib @@ -0,0 +1 @@ +