Skip to content

Commit

Permalink
Add slackUploadFile step (#614)
Browse files Browse the repository at this point in the history
  • Loading branch information
timja authored Aug 19, 2019
1 parent c44c683 commit 51c0fc3
Show file tree
Hide file tree
Showing 17 changed files with 535 additions and 86 deletions.
40 changes: 23 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ For more information about slack messages see [Slack Messages Api](https://api.s

Note the attachments API is classified as legacy, with blocks as the replacement.

#### File upload

You can upload files to slack with this plugin:
```groovy
node {
sh "echo hey > blah.txt"
slackUploadFile filePath: '*.txt', initialComment: 'HEY HEY'
}
```

This feature requires botUser mode.

#### Threads Support

You can send a message and create a thread on that message using the pipeline step.
Expand All @@ -87,11 +99,9 @@ target channel to create a thread. All messages of a thread should use the same

Example:
```
node {
def slackResponse = slackSend(channel: "cool-threads", message: "Here is the primary message")
slackSend(channel: slackResponse.threadId, message: "Thread reply #1")
slackSend(channel: slackResponse.threadId, message: "Thread reply #2")
}
def slackResponse = slackSend(channel: "cool-threads", message: "Here is the primary message")
slackSend(channel: slackResponse.threadId, message: "Thread reply #1")
slackSend(channel: slackResponse.threadId, message: "Thread reply #2")
```

This feature requires botUser mode.
Expand All @@ -100,15 +110,13 @@ Messages that are posted to a thread can also optionally be broadcasted to the
channel. Set `replyBroadcast: true` to do so. For example:

```
node {
def slackResponse = slackSend(channel: "ci", message: "Started build")
slackSend(channel: slackResponse.threadId, message: "Build still in progress")
slackSend(
channel: slackResponse.threadId,
replyBroadcast: true,
message: "Build failed. Broadcast to channel for better visibility."
)
}
def slackResponse = slackSend(channel: "ci", message: "Started build")
slackSend(channel: slackResponse.threadId, message: "Build still in progress")
slackSend(
channel: slackResponse.threadId,
replyBroadcast: true,
message: "Build failed. Broadcast to channel for better visibility."
)
```

#### Unfurling Links
Expand All @@ -118,9 +126,7 @@ You can allow link unfurling if you send the message as text. This only works in
Example:

```
node {
slackSend(channel: "news-update", message: "https://www.nytimes.com", sendAsText: true)
}
slackSend(channel: "news-update", message: "https://www.nytimes.com", sendAsText: true)
```

## Install Instructions for Slack compatible application
Expand Down
23 changes: 23 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@
<artifactId>httpclient</artifactId>
<version>4.5.9</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.9</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
Expand Down Expand Up @@ -124,6 +129,12 @@
<artifactId>workflow-cps</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-job</artifactId>
Expand All @@ -136,6 +147,18 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-durable-task-step</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.jenkins</groupId>
<artifactId>configuration-as-code</artifactId>
Expand Down
38 changes: 37 additions & 1 deletion src/main/java/jenkins/plugins/slack/CredentialsObtainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,26 @@
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import hudson.model.Item;
import hudson.model.Project;
import hudson.model.Run;
import hudson.security.ACL;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import static java.lang.String.format;

public class CredentialsObtainer {

private static final Logger logger = Logger.getLogger(CredentialsObtainer.class.getName());

public static StringCredentials lookupCredentials(String credentialId) {
List<StringCredentials> credentials = CredentialsProvider.lookupCredentials(StringCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.emptyList());
return getCredentialWithId(credentialId, credentials);
Expand Down Expand Up @@ -43,11 +54,36 @@ public static String getTokenToUse(String credentialId, Item item, String token)
}
}
if (StringUtils.isEmpty(response)) {
throw new IllegalArgumentException("the token with the provided ID could not be found and no token was specified");
throw new IllegalArgumentException(format("the credential with the provided ID (%s) could not be found and no token was specified", credentialId));
}
return response;
}

/**
* Tries to obtain the proper Item object to provide to CredentialsProvider.
* Project works for freestyle jobs, the parent of the Run works for pipelines.
* In case the proper item cannot be found, null is returned, since when null is provided to CredentialsProvider,
* it will internally use Jenkins.getInstance() which effectively only allows global credentials.
*
* @return the item to use for CredentialsProvider credential lookup
*/
@Restricted(NoExternalUse.class)
public static Item getItemForCredentials(StepContext context) {
Item item = null;
try {
item = context.get(Project.class);
if (item == null) {
Run run = context.get(Run.class);
if (run != null) {
item = run.getParent();
}
}
} catch (Exception e) {
logger.log(Level.INFO, "Exception obtaining item for credentials lookup. Only global credentials will be available", e);
}
return item;
}

private static StringCredentials getCredentialWithId(String credentialId, List<StringCredentials> credentials) {
CredentialsMatcher matcher = CredentialsMatchers.withId(credentialId);
return CredentialsMatchers.firstOrNull(credentials, matcher);
Expand Down
49 changes: 49 additions & 0 deletions src/main/java/jenkins/plugins/slack/HttpClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package jenkins.plugins.slack;

import hudson.ProxyConfiguration;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.DefaultProxyRoutePlanner;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

@Restricted(NoExternalUse.class)
public class HttpClient {

public static CloseableHttpClient getCloseableHttpClient(ProxyConfiguration proxy) {
int timeoutInSeconds = 60;

RequestConfig config = RequestConfig.custom()
.setConnectTimeout(timeoutInSeconds * 1000)
.setConnectionRequestTimeout(timeoutInSeconds * 1000)
.setSocketTimeout(timeoutInSeconds * 1000).build();

final HttpClientBuilder clientBuilder = HttpClients.custom().setDefaultRequestConfig(config);
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
clientBuilder.setDefaultCredentialsProvider(credentialsProvider);

if (proxy != null) {
final HttpHost proxyHost = new HttpHost(proxy.name, proxy.port);
final HttpRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxyHost);
clientBuilder.setRoutePlanner(routePlanner);

String username = proxy.getUserName();
String password = proxy.getPassword();
// Consider it to be passed if username specified. Sufficient?
if (username != null && !"".equals(username.trim())) {
credentialsProvider.setCredentials(new AuthScope(proxyHost),
new UsernamePasswordCredentials(username, password));
}
}
return clientBuilder.build();
}

}
41 changes: 2 additions & 39 deletions src/main/java/jenkins/plugins/slack/StandardSlackService.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,11 @@
import net.sf.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.DefaultProxyRoutePlanner;
import org.apache.http.util.EntityUtils;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;

Expand Down Expand Up @@ -313,36 +303,9 @@ private String correctEmojiFormat(String iconEmoji) {
}

protected CloseableHttpClient getHttpClient() {
int timeoutInSeconds = 60;

RequestConfig config = RequestConfig.custom()
.setConnectTimeout(timeoutInSeconds * 1000)
.setConnectionRequestTimeout(timeoutInSeconds * 1000)
.setSocketTimeout(timeoutInSeconds * 1000).build();

final HttpClientBuilder clientBuilder = HttpClients.custom().setDefaultRequestConfig(config);
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
clientBuilder.setDefaultCredentialsProvider(credentialsProvider);

Jenkins jenkins = Jenkins.getInstanceOrNull();
if (jenkins != null) {
ProxyConfiguration proxy = jenkins.proxy;
if (proxy != null) {
final HttpHost proxyHost = new HttpHost(proxy.name, proxy.port);
final HttpRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxyHost);
clientBuilder.setRoutePlanner(routePlanner);

String username = proxy.getUserName();
String password = proxy.getPassword();
// Consider it to be passed if username specified. Sufficient?
if (username != null && !"".equals(username.trim())) {
logger.info("Using proxy authentication (user=" + username + ")");
credentialsProvider.setCredentials(new AuthScope(proxyHost),
new UsernamePasswordCredentials(username, password));
}
}
}
return clientBuilder.build();
ProxyConfiguration proxy = jenkins != null ? jenkins.proxy : null;
return HttpClient.getCloseableHttpClient(proxy);
}

void setHost(String host) {
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/jenkins/plugins/slack/pipeline/SlackFileRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package jenkins.plugins.slack.pipeline;

import hudson.FilePath;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

@Restricted(NoExternalUse.class)
public class SlackFileRequest {
private final String fileToUploadPath;
private final String token;
private final String channels;

private final String initialComment;
private final FilePath filePath;

public SlackFileRequest(FilePath filePath, String token, String channels, String initialComment, String fileToUploadPath) {
this.token = token;
this.channels = channels;
this.initialComment = initialComment;
this.filePath = filePath;
this.fileToUploadPath = fileToUploadPath;
}

public String getToken() {
return token;
}

public String getChannels() {
return channels;
}

public String getInitialComment() {
return initialComment;
}

public FilePath getFilePath() {
return filePath;
}

public String getFileToUploadPath() {
return fileToUploadPath;
}
}
Loading

0 comments on commit 51c0fc3

Please sign in to comment.