Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.activation.MimeTypeParseException;
import javax.annotation.CheckForNull;

import org.kohsuke.stapler.StaplerRequest;
Expand Down Expand Up @@ -171,15 +172,30 @@ public void migrateData()
@Override
public boolean configure(StaplerRequest staplerRequest, JSONObject json) throws FormException
{

// when we bind the stapler request we get a new instance of logstashIndexer.
// logstashIndexer is holder for the dao instance.
// To avoid that we get a new dao instance in case there was no change in configuration
// we compare it to the currently active configuration.
staplerRequest.bindJSON(this, json);

try {
// validate
logstashIndexer.validate();
} catch (Exception ex) {
// You are here which means user is trying to save invalid indexer configuration.
// Exception will be thrown here so that it gets displayed on UI.
// But before that revert back to original configuration (in-memory)
// so that when user refreshes the configuration page, last saved settings will be displayed again.
logstashIndexer = activeIndexer;
throw new IllegalArgumentException(ex);
}

if (!Objects.equals(logstashIndexer, activeIndexer))
{
activeIndexer = logstashIndexer;
}

save();
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import java.net.URISyntaxException;
import java.net.URL;

import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
Expand All @@ -21,6 +24,7 @@ public class ElasticSearch extends LogstashIndexer<ElasticSearchDao>
private String username;
private Secret password;
private URI uri;
private String mimeType;

@DataBoundConstructor
public ElasticSearch()
Expand All @@ -32,6 +36,10 @@ public URI getUri()
return uri;
}

@Override
public void validate() throws MimeTypeParseException {
new MimeType(this.mimeType);
}

/*
* We use URL for the setter as stapler can autoconvert a string to a URL but not to a URI
Expand Down Expand Up @@ -68,7 +76,16 @@ public void setPassword(String password)
{
this.password = Secret.fromString(password);
}


@DataBoundSetter
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}

public String getMimeType() {
return mimeType;
}

@Override
public boolean equals(Object obj)
{
Expand Down Expand Up @@ -101,6 +118,10 @@ else if (!username.equals(other.username))
{
return false;
}
else if (!mimeType.equals(other.mimeType))
{
return false;
}
return true;
}

Expand All @@ -118,7 +139,9 @@ public int hashCode()
@Override
public ElasticSearchDao createIndexerInstance()
{
return new ElasticSearchDao(getUri(), username, Secret.toString(password));
ElasticSearchDao esDao = new ElasticSearchDao(getUri(), username, Secret.toString(password));
esDao.setMimeType(getMimeType());
return esDao;
}

@Extension
Expand Down Expand Up @@ -163,5 +186,17 @@ public FormValidation doCheckUrl(@QueryParameter("value") String value)
}
return FormValidation.ok();
}
public FormValidation doCheckMimeType(@QueryParameter("value") String value) {
if (StringUtils.isBlank(value)) {

Choose a reason for hiding this comment

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

I think a blank mimeType should be valid. In that case it should be set to null in the DAO so you have the old behaviour. This should be mentioned in the help.
You can also save with a blank mimeType which would result in no mimeType being configured in the StringEntity. Not sure if this will break anything,

Copy link
Author

Choose a reason for hiding this comment

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

@mwinter69 IMHO, I think we should not allow user to save config with blank mimeType and it would not be of much help/value addition if we do not configure mimeType on StringEntity considering the nature of elasticsearch and logstash. I think we should set application/json by default which will make both ES and logstash happy.

Choose a reason for hiding this comment

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

Does this also fail when you try to save with an empty string? If not then you can run into problems at runtime as you will pass an empty string. Maybe use Util.fixEmptyAndTrim in the DAO to avoid this or do not allow to save with an illegal mimetype.

Copy link
Author

@VamanKulkarni VamanKulkarni Mar 14, 2018

Choose a reason for hiding this comment

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

@mwinter69 Apologies for delays in response. Recovering from bad health. Correct me if I mis-understand understand your question. As per current logic, user is not allowed to save an empty string for mimeType. It doesn't displays an error if user tries to save empty/invalid mimeType.

Choose a reason for hiding this comment

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

Go to the configuration page and make the mimetype field empty. When you leave the field it will tell you that a value is required. Now click on apply. Congratulations you have set the mimetype to an empty string which will most likely fail.
You should throw an exception in the setter method if no valid mimetype is given.

Choose a reason for hiding this comment

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

Looking into this it seems that Stapler is swallowing the exception and only logging when you try to use a setter method,
By initializing the mimetype in the constructor, and throwing the exception there you can avoid that it gets saved.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the inputs. I am hoping that I understood your suggestion correctly. Here is what I did. I initialized mimeType in ElasticSearchDao() constructor and if it is invalid, thrown an exception just like how its done for uri.toURL(); currently. However, I realized that ElasticSeachDao is not instantiated every time we click on "Save" button on jenkins configuration page. Hence throwing exception in side ElasticSearchDao() did not work for me meaning UI does not show any exception. It only throws exception when you run a job.

In an alternative approach and also you mentioned this in one of your earlier comments that we can allow blank mimeType. If the mimeType is blank we can set it to null in ElasticSearch and with current implementation if mimeType is null it will be set to default application/json value. Does this sound acceptable?

Choose a reason for hiding this comment

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

The dao is the wrong place to do the checks. You have to do it in the constructor of configuration/ElasticSearch. Alternatively one could extend the base class LogstashIndexer with a validate method and call the validate method in the LogstashConfiguration.configure before save()

Copy link
Author

Choose a reason for hiding this comment

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

@mwinter69 Pushed updated code.

Copy link
Author

Choose a reason for hiding this comment

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

@jakub-bochenski Updated code with your review comments

return FormValidation.error(Messages.ValueIsRequired());
}
try {
//This is simply to check validity of the given mimeType
new MimeType(value);
} catch (MimeTypeParseException e) {
return FormValidation.error(Messages.ProvideValidMimeType());
}
return FormValidation.ok();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ public synchronized T getInstance()
return instance;
}

/**
* Purpose of this method is to validate the inputs (if required) and if found
* erroneous throw an exception so that it will be bubbled up to the UI.
*
* @throws Exception
*/
public void validate() throws Exception {

Choose a reason for hiding this comment

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

[minor] it would be better to have something more specific than Exception in the signature (e.g. ConstraintViolationException); OTOH it would force us to catch and wrap MimeTypeParseException, so I'm not sure if it's worth it

Copy link
Author

Choose a reason for hiding this comment

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

OTOH it would force us to catch and wrap MimeTypeParseException, so I'm not sure if it's worth it

Thats the reason I didn't use specific exception there. In this case I feel we should not bother about the specific exception. If validate() throws any exception we know it is due to invalid indexer config so simply displaying wrapped exception would be sufficient . Let me know if you feel there is a value in that I can make a change.

}



/**
* Creates a new {@link AbstractLogstashIndexerDao} instance corresponding to this configuration.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,32 @@
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import com.google.common.collect.Range;

import jenkins.plugins.logstash.configuration.ElasticSearch;


/**
* Elastic Search Data Access Object.
*
* @author Liam Newman
* @since 1.0.4
*/
public class ElasticSearchDao extends AbstractLogstashIndexerDao {

private final HttpClientBuilder clientBuilder;
private final URI uri;
private final String auth;
private final Range<Integer> successCodes = closedOpen(200,300);

private String username;
private String password;
private String mimeType;


//primary constructor used by indexer factory
public ElasticSearchDao(URI uri, String username, String password) {
Expand Down Expand Up @@ -102,11 +107,11 @@ public ElasticSearchDao(URI uri, String username, String password) {
clientBuilder = factory == null ? HttpClientBuilder.create() : factory;
}


public URI getUri()
{
return uri;
}

public String getHost()
{
return uri.getHost();
Expand Down Expand Up @@ -136,16 +141,27 @@ public String getKey()
{
return uri.getPath();
}


public String getMimeType() {
return this.mimeType;
}

public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}

String getAuth()
{
return auth;
}

protected HttpPost getHttpPost(String data) {
HttpPost postRequest;
postRequest = new HttpPost(uri);
StringEntity input = new StringEntity(data, ContentType.APPLICATION_JSON);
HttpPost getHttpPost(String data) {
HttpPost postRequest = new HttpPost(uri);
String mimeType = this.getMimeType();
// char encoding is set to UTF_8 since this request posts a JSON string
StringEntity input = new StringEntity(data, StandardCharsets.UTF_8);
mimeType = (mimeType != null) ? mimeType : ContentType.APPLICATION_JSON.toString();

Choose a reason for hiding this comment

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

mimeType will never be null. If you save the global configuration with no content for the mimeType you will get an empty string. Only when you update the plugin without ever saving the configuration it will be null.

input.setContentType(mimeType);
postRequest.setEntity(input);
if (auth != null) {
postRequest.addHeader("Authorization", "Basic " + auth);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@
DisplayName = Send console log to Logstash
ValueIsInt = Value must be an integer
ValueIsRequired = Value is required
PleaseProvideHost = Please set a valid host name
PleaseProvideHost = Please set a valid host name
ProvideValidMimeType = Please provide a valid mime type
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@
<f:entry title="${%Password}" field="password">
<f:password/>
</f:entry>
<f:entry title="${%Mime Type}" field="mimeType">
<f:textbox default="application/json"/>
</f:entry>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
<p>
MIME type of the request body that is sent to ELASTICSEARCH indexer. It should be of the form <b>type/subtype</b> e.g. <i>application/json</i><br>
Since this is a field for MIME Type and not Content-Type we do not support additional content-type paramters like <b>charset</b> or <b>boundry</b>
</p>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ public void setup() throws MalformedURLException, URISyntaxException
indexer.setUri(url);
indexer.setPassword("password");
indexer.setUsername("user");
indexer.setMimeType("application/json");

indexer2 = new ElasticSearch();
indexer2.setUri(url);
indexer2.setPassword("password");
indexer2.setUsername("user");
indexer2.setMimeType("application/json");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ ElasticSearchDao createDao(String url, String username, String password) throws
public void before() throws Exception {
int port = (int) (Math.random() * 1000);
dao = createDao("http://localhost:8200/logstash", "username", "password");

when(mockClientBuilder.build()).thenReturn(mockHttpClient);
when(mockHttpClient.execute(any(HttpPost.class))).thenReturn(mockResponse);
when(mockResponse.getStatusLine()).thenReturn(mockStatusLine);
Expand Down Expand Up @@ -196,6 +196,24 @@ public void pushFailStatusCode() throws Exception {
e.getMessage().contains("Something bad happened.") && e.getMessage().contains("HTTP error code: 500"));
throw e;
}


}
@Test
public void getHttpPostSuccessWithUserInput() throws Exception {
String json = "{ 'foo': 'bar' }";
String mimeType = "application/json";
dao = createDao("http://localhost:8200/jenkins/logstash", "username", "password");
dao.setMimeType(mimeType);
HttpPost post = dao.getHttpPost(json);
HttpEntity entity = post.getEntity();
assertEquals("Content type do not match", mimeType, entity.getContentType().getValue());
}
@Test
public void getHttpPostWithFallbackInput() throws Exception {
String json = "{ 'foo': 'bar' }";
dao = createDao("http://localhost:8200/jenkins/logstash", "username", "password");
HttpPost post = dao.getHttpPost(json);
HttpEntity entity = post.getEntity();
assertEquals("Content type do not match", ContentType.APPLICATION_JSON.toString(), entity.getContentType().getValue());
}
}