Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,27 @@ Make sure your Locale is set to en_US, otherwise the expect script wont work as

## Usage

### Configure RPM signing keys
* Go to Jenkins >> Manage Jenkins >> Configure System
* Go to "RPM Signing Keys" section
* Click on "Add GPG key" button to configure gpg key in jenkins master.

## Option-1: Using Jenkinsfile (or pipeline)
* Use Jenkins "Pipeline Syntax" (Snippet Generator) to help generate pipeline step.
* Select "rpmSign: [RPMSign] - Sign RPMs" from "Sample Step" drop down.
* Set values for "Sign RPMs"
* "GPG Key" drop down to select value from the configured GPG keys (mentioned in above section).
* "includes" textbox to provide rpm paths (default value is \*\*/target/*.rpm).
* "Cmdline Options" for custom options to be passed to rpm command.
* "Resign?" checkbox (enable it if resigning of rpm is required).
* Click on "Generate Pipeline Script"
* genereated step example:
```
rpmSign(rpms: [[gpgKeyName: '121ADA11', includes: 'build/distributions/*.rpm']]])
```

## Option-2: USing Jenkins job configuration
* Click on "Configure" button of jenkins job.
* Select "[RPMSign] - Sign RPMs" from "Post-build Action" drop down.
* Click on "Add RPM" button.
* Set values for GPG Key, includes, Cmdline Options, and Resign.
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>3.4</version>
<version>3.26</version>
</parent>

<artifactId>rpmsign-plugin</artifactId>
Expand All @@ -14,8 +14,8 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jenkins.version>1.625.3</jenkins.version>
<java.level>7</java.level>
<jenkins.version>2.138.2</jenkins.version>
<java.level>8</java.level>
</properties>

<build>
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/jenkins/plugins/rpmsign/GpgKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public String getId() {

public int getUniqueId() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (privateKey.getPlainText() != null ? privateKey.getPlainText().hashCode() : 0);
result = 31 * result + (passphrase.getPlainText() != null ? passphrase.getPlainText().hashCode() : 0);
result = 31 * result + privateKey.getPlainText().hashCode();
result = 31 * result + passphrase.getPlainText().hashCode();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove this null check?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can not be null.
getPlainText() method of Secret class uses value field and it is annotated as @nonnull.

return result;
}

Expand Down
37 changes: 33 additions & 4 deletions src/main/java/jenkins/plugins/rpmsign/Rpm.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

public class Rpm extends AbstractDescribableImpl<Rpm> {

private final String gpgKeyName;
private final String includes;
private final String cmdlineOpts;
private final boolean resign;
private String gpgKeyName;
private String includes;
private String cmdlineOpts;
private boolean resign;

@DataBoundConstructor
public Rpm() {
this.gpgKeyName = "";
this.includes = "**/**.rpm";
this.resign = false;
this.cmdlineOpts = "";
}

@Deprecated
public Rpm(String gpgKeyName, String includes, String cmdlineOpts, boolean resign) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why deprecate the old constructor? Do pipeline uses of this plugin use the setters instead of the constructor? Wouldn't this constructor still be appropriate for non-pipeline configurations?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pipeline uses constructor, and setter annotated with @DataBoundConstructor, @DataBoundSetter annotations respectively.
that's a valid point. We'll have to check if it's required for non-pipeline configurations.

this.gpgKeyName = gpgKeyName;
this.includes = includes;
Expand All @@ -36,6 +45,26 @@ public boolean isResign() {
return resign;
}

@DataBoundSetter
public void setGpgKeyName(final String gpgKeyName) {
this.gpgKeyName = gpgKeyName;
}

@DataBoundSetter
public void setIncludes(final String includes) {
this.includes = includes;
}

@DataBoundSetter
public void setCmdlineOpts(final String cmdlineOpts) {
this.cmdlineOpts = cmdlineOpts;
}

@DataBoundSetter
public void setResign(final boolean resign) {
this.resign = resign;
}

@Extension
public static class DescriptorImpl extends Descriptor<Rpm> {

Expand Down
75 changes: 42 additions & 33 deletions src/main/java/jenkins/plugins/rpmsign/RpmSignPlugin.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package jenkins.plugins.rpmsign;

import hudson.AbortException;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
Expand All @@ -8,6 +9,8 @@
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
Expand All @@ -16,11 +19,14 @@
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import jenkins.model.Jenkins;
import jenkins.tasks.SimpleBuildStep;
import net.sf.json.JSONObject;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;

Expand All @@ -30,17 +36,23 @@
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import javax.annotation.Nonnull;

public class RpmSignPlugin extends Recorder {
public class RpmSignPlugin extends Recorder implements SimpleBuildStep {

private List<Rpm> entries = Collections.emptyList();
private List<Rpm> rpms;

@DataBoundConstructor
@SuppressWarnings("unused")
public RpmSignPlugin(List<Rpm> rpms) {
this.entries = rpms;
if (this.entries == null) {
this.entries = Collections.emptyList();
setRpms(rpms);
}

@DataBoundSetter
public void setRpms(final List<Rpm> rpms) {
this.rpms = rpms;
if (this.rpms == null) {
this.rpms = Collections.emptyList();
}
}

Expand All @@ -49,49 +61,45 @@ public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}

private boolean isPerformDeployment(AbstractBuild build) {
private boolean isPerformDeployment(final Run<?, ?> build) {
Result result = build.getResult();
return result == null || result.isBetterOrEqualTo(Result.UNSTABLE);

}

@SuppressWarnings("unused")
public List<Rpm> getEntries() {
return entries;
public List<Rpm> getRpms() {
return rpms;
}

@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
public void perform(@Nonnull final Run<?, ?> build, @Nonnull final FilePath workspace, @Nonnull final Launcher launcher, @Nonnull final TaskListener listener) throws InterruptedException, IOException {
if (isPerformDeployment(build)) {
listener.getLogger().println("[RpmSignPlugin] - Starting signing RPMs ...");

for (Rpm rpmEntry : entries) {
for (Rpm rpmEntry : rpms) {
StringTokenizer rpmGlobTokenizer = new StringTokenizer(rpmEntry.getIncludes(), ",");

GpgKey gpgKey = getGpgKey(rpmEntry.getGpgKeyName());
if (gpgKey == null) {
throw new InterruptedException("No GPG key is available.");
throw new AbortException("No GPG key is available.");
}
if (gpgKey.getPrivateKey().getPlainText().length() > 0) {
listener.getLogger().println("[RpmSignPlugin] - Importing private key");
importGpgKey(gpgKey.getPrivateKey().getPlainText(), build, launcher, listener);
importGpgKey(gpgKey.getPrivateKey().getPlainText(), build, workspace, launcher, listener);
listener.getLogger().println("[RpmSignPlugin] - Imported private key");
}

if (!isGpgKeyAvailable(gpgKey, build, launcher, listener)) {
if (!isGpgKeyAvailable(gpgKey, build, workspace, launcher, listener)) {
listener.getLogger().println("[RpmSignPlugin] - Can't find GPG key: " + rpmEntry.getGpgKeyName());
return false;
throw new AbortException("Can't find GPG key: " + rpmEntry.getGpgKeyName());
}

while (rpmGlobTokenizer.hasMoreTokens()) {
String rpmGlob = rpmGlobTokenizer.nextToken();

listener.getLogger().println("[RpmSignPlugin] - Publishing " + rpmGlob);

FilePath workspace = build.getWorkspace();
if (workspace == null) {
throw new IllegalStateException("Could not get a workspace.");
}
FilePath[] matchedRpms = workspace.list(rpmGlob);
if (ArrayUtils.isEmpty(matchedRpms)) {
listener.getLogger().println("[RpmSignPlugin] - No RPMs matching " + rpmGlob);
Expand Down Expand Up @@ -119,7 +127,7 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
int returnCode = proc.join();
if (returnCode != 0) {
listener.getLogger().println(logPrefix + "Failed signing RPM ...");
return false;
throw new AbortException("Failed signing RPM. returnCode: " + returnCode);
}
i++;
}
Expand All @@ -131,6 +139,15 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
} else {
listener.getLogger().println("[RpmSignPlugin] - Skipping signing RPMs ...");
}
}

@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
FilePath workspace = build.getWorkspace();
if (workspace == null) {
throw new AbortException("Could not get a workspace.");
}
perform(build, workspace, launcher, listener);
return true;
}

Expand Down Expand Up @@ -178,12 +195,12 @@ private byte[] createExpectScriptFile(String signCommand, String passphrase)
return baos.toByteArray();
}

private void importGpgKey(String privateKey, AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
private void importGpgKey(String privateKey, Run<?, ?> build, final FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException {
ArgumentListBuilder command = new ArgumentListBuilder();
command.add("gpg", "--import", "-");
Launcher.ProcStarter ps = launcher.new ProcStarter();
ps = ps.cmds(command).stdout(listener);
ps = ps.pwd(build.getWorkspace()).envs(build.getEnvironment(listener));
ps = ps.pwd(workspace).envs(build.getEnvironment(listener));

try (InputStream is = new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8))) {
ps.stdin(is);
Expand All @@ -192,19 +209,19 @@ private void importGpgKey(String privateKey, AbstractBuild<?, ?> build, Launcher
}
}

private boolean isGpgKeyAvailable(GpgKey gpgKey, AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
private boolean isGpgKeyAvailable(GpgKey gpgKey, Run<?, ?> build, final FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
ArgumentListBuilder command = new ArgumentListBuilder();
command.add("gpg", "--fingerprint", gpgKey.getName());
Launcher.ProcStarter ps = launcher.new ProcStarter();
ps = ps.cmds(command).stdout(listener);
ps = ps.pwd(build.getWorkspace()).envs(build.getEnvironment(listener));
ps = ps.pwd(workspace).envs(build.getEnvironment(listener));
Proc proc = launcher.launch(ps);

return proc.join() == 0;
}

private GpgKey getGpgKey(String gpgKeyName) {
Jenkins jenkins = Jenkins.getInstance();
Jenkins jenkins = Jenkins.get();
if (jenkins == null) {
throw new IllegalStateException("Could not get a Jenkins instance.");
}
Expand All @@ -220,6 +237,7 @@ private GpgKey getGpgKey(String gpgKeyName) {
}

@Extension
@Symbol("rpmSign")
@SuppressWarnings("unused")
public static final class GpgSignerDescriptor extends BuildStepDescriptor<Publisher> {

Expand Down Expand Up @@ -273,16 +291,7 @@ public FormValidation doCheckPassphrase(@AncestorInPath AbstractProject project,
}

public FormValidation doCheckIncludes(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException, InterruptedException {
FilePath workspace = project.getSomeWorkspace();
if (workspace != null) {
String msg = workspace.validateAntFileMask(value);
if (msg != null) {
return FormValidation.error(msg);
}
return FormValidation.ok();
} else {
return FormValidation.warning(Messages.noworkspace());
}
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<f:entry title="${%Sign RPMs}">

<f:repeatable var="rpms" items="${instance.entries}" minimum="${1}" add="${%Add RPM(s)}">
<f:repeatable var="rpms" items="${instance.rpms}" minimum="${1}" add="${%Add RPM(s)}">

<table width="100%">

Expand Down