forked from splunk/splunkforjenkins
-
Notifications
You must be signed in to change notification settings - Fork 39
/
SplunkJenkinsInstallation.java
674 lines (597 loc) · 22.7 KB
/
SplunkJenkinsInstallation.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
package com.splunk.splunkjenkins;
import com.splunk.splunkjenkins.model.EventType;
import com.splunk.splunkjenkins.model.MetaDataConfigItem;
import com.splunk.splunkjenkins.utils.SplunkLogService;
import groovy.lang.GroovyCodeSource;
import hudson.Extension;
import hudson.Util;
import hudson.util.FormValidation;
import jenkins.model.GlobalConfiguration;
import jenkins.model.Jenkins;
import jenkins.model.JenkinsLocationConfiguration;
import net.sf.json.JSONObject;
import org.acegisecurity.Authentication;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.scriptsecurity.scripts.ApprovalContext;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.interceptor.RequirePOST;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static com.splunk.splunkjenkins.Constants.*;
import static com.splunk.splunkjenkins.utils.LogEventHelper.getDefaultDslScript;
import static com.splunk.splunkjenkins.utils.LogEventHelper.nonEmpty;
import static com.splunk.splunkjenkins.utils.LogEventHelper.validateGroovyScript;
import static com.splunk.splunkjenkins.utils.LogEventHelper.verifyHttpInput;
import static groovy.lang.GroovyShell.DEFAULT_CODE_BASE;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.apache.commons.lang.StringUtils.isNotEmpty;
@Extension
public class SplunkJenkinsInstallation extends GlobalConfiguration {
private static transient boolean logHandlerRegistered = false;
private transient static final Logger LOG = Logger.getLogger(SplunkJenkinsInstallation.class.getName());
private transient volatile static SplunkJenkinsInstallation cachedConfig;
private transient static final Pattern uuidPattern = Pattern.compile("[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}", CASE_INSENSITIVE);
// Defaults plugin global config values
private boolean enabled = false;
private String host;
private String token;
private boolean useSSL = true;
private Integer port = 8088;
//for console log default cache size for 256KB
private long maxEventsBatchSize = 1 << 18;
private long retriesOnError = 3;
private boolean rawEventEnabled = true;
//groovy script path
private String scriptPath;
private String metaDataConfig;
//groovy content if file path not set
private String scriptContent;
//the app-jenkins link
private String splunkAppUrl;
private String metadataHost;
private String metadataSource;
private String ignoredJobs;
private Boolean globalPipelineFilter;
//below are all transient properties
public transient Properties metaDataProperties = new Properties();
//cached values, will not be saved to disk!
private transient String jsonUrl;
private transient String rawUrl;
private transient File scriptFile;
private transient long scriptTimestamp;
private transient String postActionScript;
private transient Set<MetaDataConfigItem> metadataItemSet = new HashSet<>();
private transient String defaultMetaData;
private transient Pattern ignoredJobPattern;
public SplunkJenkinsInstallation(boolean useConfigFile) {
if (useConfigFile) {
load();
}
}
@Override
public synchronized final void load() {
super.load();
migrate();
//load default metadata
try (InputStream metaInput = this.getClass().getClassLoader().getResourceAsStream("metadata.properties")) {
defaultMetaData = IOUtils.toString(metaInput);
} catch (IOException e) {
//ignore
}
}
public SplunkJenkinsInstallation() {
this(true);
}
public static SplunkJenkinsInstallation get() {
if (cachedConfig == null) {
if (Jenkins.getInstanceOrNull() == null) {
// Jenkins is not ready yet
return buildTempInstance();
}
synchronized (SplunkJenkinsInstallation.class) {
if (cachedConfig == null) {
cachedConfig = (SplunkJenkinsInstallation) Jenkins.getInstance().getDescriptor(SplunkJenkinsInstallation.class);
if (cachedConfig == null) {
return buildTempInstance();
}
}
}
}
return cachedConfig;
}
// a temp instance with disabled flag
private static SplunkJenkinsInstallation buildTempInstance() {
SplunkJenkinsInstallation temp = new SplunkJenkinsInstallation(false);
temp.enabled = false;
return temp;
}
/**
* @return true if the plugin had been setup by Jenkins (constructor had been called)
*/
public static boolean isLogHandlerRegistered() {
return logHandlerRegistered && Jenkins.getInstance() != null;
}
/**
* mark this plugin as initiated
*
* @param completed mark the init as initiate completed
*/
public static void markComplete(boolean completed) {
logHandlerRegistered = completed;
}
/**
* Note: this method is meant to be called on agent only!
*
* @param config the SplunkJenkinsInstallation to be used on Agent
*/
public static void initOnAgent(SplunkJenkinsInstallation config) {
SplunkJenkinsInstallation.cachedConfig = config;
config.updateCache();
}
@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
this.metadataItemSet = null; // otherwise bindJSON will never clear it once set
boolean previousState = this.enabled;
req.bindJSON(this, formData);
if (this.metadataItemSet == null) {
this.metaDataConfig = "";
}
//handle choice
if ("file".equals(formData.get("commandsOrFileInSplunkins"))) {
this.scriptContent = null;
} else {
this.scriptPath = null;
}
updateCache();
save();
if (previousState && !this.enabled) {
//switch from enable to disable
SplunkLogService.getInstance().stopWorker();
SplunkLogService.getInstance().releaseConnection();
}
return true;
}
/*
* Form validation methods
*/
@RequirePOST
public FormValidation doCheckHost(@QueryParameter("value") String hostName) {
if (StringUtils.isBlank(hostName)) {
return FormValidation.warning(Messages.PleaseProvideHost());
} else if (hostName.startsWith("http://") || hostName.startsWith("https://")) {
try {
URI uri = new URI(hostName);
String domain = uri.getHost();
return FormValidation.warning(Messages.HostNameSchemaWarning(domain));
} catch (URISyntaxException e) {
return FormValidation.warning(Messages.HostNameInvalid());
}
} else if ((hostName.endsWith("cloud.splunk.com") || hostName.endsWith("splunkcloud.com")
|| hostName.endsWith("splunktrial.com")) &&
!(hostName.startsWith("input-") || hostName.startsWith("http-inputs-"))) {
return FormValidation.warning(Messages.CloudHostPrefix(hostName));
} else {
return FormValidation.ok();
}
}
@RequirePOST
public FormValidation doCheckToken(@QueryParameter("value") String value) {
//check GUID format such as 18654C68-B28B-4450-9CF0-6E7645CA60CA
if (StringUtils.isBlank(value) || !uuidPattern.matcher(value).find()) {
return FormValidation.warning(Messages.InvalidToken());
}
return FormValidation.ok();
}
@RequirePOST
public FormValidation doTestHttpInput(@QueryParameter String host, @QueryParameter int port,
@QueryParameter String token, @QueryParameter boolean useSSL,
@QueryParameter String metaDataConfig) {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
//create new instance to avoid pollution global config
SplunkJenkinsInstallation config = new SplunkJenkinsInstallation(false);
config.host = host;
config.port = port;
config.token = token;
config.useSSL = useSSL;
config.metaDataConfig = metaDataConfig;
config.enabled = true;
config.updateCache();
if (!config.isValid()) {
return FormValidation.error(Messages.InvalidHostOrToken());
}
return verifyHttpInput(config);
}
@RequirePOST
public FormValidation doCheckScriptContent(@QueryParameter String value) {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
if (StringUtils.isBlank(value)) {
return FormValidation.ok();
}
return validateGroovyScript(value);
}
@RequirePOST
public FormValidation doCheckMaxEventsBatchSize(@QueryParameter int value) {
if (value < MIN_BUFFER_SIZE || value > MAX_BATCH_SIZE) {
return FormValidation.error(String.format("please consider a value between %d and %d", MIN_BUFFER_SIZE, MAX_BATCH_SIZE));
}
return FormValidation.ok();
}
@RequirePOST
public FormValidation doCheckIgnoredJobs(@QueryParameter String value) {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
try {
Pattern.compile(value);
} catch (PatternSyntaxException ex) {
return FormValidation.errorWithMarkup(Messages.InvalidPattern());
}
return FormValidation.ok();
}
////////END OF FORM VALIDATION/////////
protected void updateCache() {
if (!this.enabled) {
//nothing to do if not enabled
return;
}
if (scriptPath != null) {
scriptFile = new File(scriptPath);
//load the text content into postActionScript
refreshScriptText();
} else if (nonEmpty(scriptContent)) {
postActionScript = scriptContent;
scriptTimestamp = System.currentTimeMillis();
checkApprove(postActionScript);
} else {
postActionScript = null;
scriptTimestamp = 0;
}
if (StringUtils.isEmpty(ignoredJobs)) {
ignoredJobPattern = null;
} else {
try {
ignoredJobPattern = Pattern.compile(ignoredJobs);
} catch (PatternSyntaxException ex) {
LOG.log(Level.SEVERE, "invalid ignore job pattern {0}, error: {1}", new Object[]{
ignoredJobs, ex.getDescription()});
}
}
try {
String scheme = useSSL ? "https://" : "http://";
jsonUrl = scheme + host + ":" + port + JSON_ENDPOINT;
rawUrl = scheme + host + ":" + port + RAW_ENDPOINT;
//discard previous metadata cache and load new one
metaDataProperties = new Properties();
String combinedMetaData = Util.fixNull(defaultMetaData) + "\n" + Util.fixNull(metaDataConfig);
if (!isEmpty(combinedMetaData)) {
metaDataProperties.load(new StringReader(combinedMetaData));
}
if (isNotEmpty(metadataSource)) {
metaDataProperties.put("source", metadataSource);
}
} catch (Exception e) {
LOG.log(Level.SEVERE, "update cache failed, splunk host:" + host, e);
}
}
private void checkApprove(String scriptText) {
if (scriptText == null) {
return;
}
// During startup, hudson.model.User.current() calls User.load which will load other plugins, will throw error:
// Tried proxy for com.splunk.splunkjenkins.SplunkJenkinsInstallation to support a circular dependency, but it is not an interface.
// Use Jenkins.getAuthentication() will by pass the issue
Authentication auth = Jenkins.getAuthentication();
String userName = auth.getName();
ApprovalContext context = ApprovalContext.create().withUser(userName).withKey(this.getClass().getName());
//check approval and save pending for admin approval
ScriptApproval.get().configuring(scriptText, GroovyLanguage.get(), context);
}
/**
* Reload script content from file if modified
*/
private void refreshScriptText() {
if (scriptFile == null) {
return;
}
try {
if (!scriptFile.canRead()) {
postActionScript = null;
} else {
scriptTimestamp = scriptFile.lastModified();
postActionScript = IOUtils.toString(scriptFile.toURI());
checkApprove(postActionScript);
}
} catch (IOException e) {
LOG.log(Level.SEVERE, "can not read file " + scriptFile, e);
//file was removed from jenkins, just ignore
}
}
/**
* check if configured correctly
*
* @return true setup is completed
*/
public boolean isValid() {
return enabled && host != null && token != null
&& jsonUrl != null && rawUrl != null;
}
/**
* get cached script contents
*
* @return script content
*/
public String getScript() {
if (scriptPath != null && scriptFile.lastModified() > scriptTimestamp) {
refreshScriptText();
}
return this.postActionScript;
}
public GroovyCodeSource getCode() {
String script = getScript();
GroovyCodeSource codeSource = new GroovyCodeSource(script, "SplunkinUserScript" + scriptTimestamp, DEFAULT_CODE_BASE);
return codeSource;
}
public boolean isRawEventEnabled() {
return rawEventEnabled;
}
/**
* Check whether we can optimize sending process, e.g. if we need to send 1000 lines for one job console log,
* and we can specify host,source,sourcetype,index only once in query parameter if raw event is supported,
* instead of sending 1000 times in request body
*
* @param eventType does this type of text need to be logged to splunk line by line
* @return true if HEC supports specify metadata in url query parameter
*/
public boolean canPostRaw(EventType eventType) {
return rawEventEnabled && eventType.needSplit();
}
public String getToken() {
return token;
}
public long getMaxRetries() {
return retriesOnError;
}
/**
* @param keyName such as host,source,index
* @return the configured metadata
*/
public String getMetaData(String keyName) {
return metaDataProperties.getProperty(keyName);
}
public String getJsonUrl() {
return jsonUrl;
}
public String getRawUrl() {
return rawUrl;
}
public boolean isEnabled() {
return enabled;
}
public boolean isEventDisabled(EventType eventType) {
return !isValid() || metaDataProperties == null || "false".equals(metaDataProperties.getProperty(eventType.getKey("enabled")));
}
public boolean isJobIgnored(String jobUrl) {
boolean ignored = false;
if (JOB_CONSOLE_FILTER_WHITELIST_PATTERN != null) {
// white list via system properties
if (!JOB_CONSOLE_FILTER_WHITELIST_PATTERN.matcher(jobUrl).find()) {
LOG.log(Level.FINE, "{0} is not in whitelist set by splunkins.allowConsoleLogPattern", jobUrl);
ignored = true;
}
}
if (!ignored && ignoredJobPattern != null) {
// black list
ignored = ignoredJobPattern.matcher(jobUrl).find();
}
return ignored;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public void setToken(String token) {
this.token = token;
}
public boolean isUseSSL() {
return useSSL;
}
public void setUseSSL(boolean useSSL) {
this.useSSL = useSSL;
}
public long getMaxEventsBatchSize() {
return maxEventsBatchSize;
}
public void setMaxEventsBatchSize(long maxEventsBatchSize) {
if (maxEventsBatchSize > MIN_BUFFER_SIZE) {
this.maxEventsBatchSize = maxEventsBatchSize;
} else {
this.maxEventsBatchSize = MIN_BUFFER_SIZE;
}
}
public void setRawEventEnabled(boolean rawEventEnabled) {
this.rawEventEnabled = rawEventEnabled;
}
public String getMetaDataConfig() {
return metaDataConfig;
}
public void setMetaDataConfig(String metaDataConfig) {
this.metaDataConfig = metaDataConfig;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public long getRetriesOnError() {
return retriesOnError;
}
public void setRetriesOnError(long retriesOnError) {
this.retriesOnError = retriesOnError;
}
public String getScriptPath() {
return scriptPath;
}
public void setScriptPath(String scriptPath) {
this.scriptPath = scriptPath;
}
public String getScriptContent() {
return scriptContent;
}
public void setScriptContent(String scriptContent) {
this.scriptContent = scriptContent;
}
public Map toMap() {
HashMap map = new HashMap();
map.put("token", this.token);
map.put("rawEventEnabled", this.rawEventEnabled);
map.put("maxEventsBatchSize", this.maxEventsBatchSize);
map.put("host", host);
map.put("port", port);
map.put("useSSL", useSSL);
map.put("metaDataConfig", Util.fixNull(defaultMetaData) + Util.fixNull(metaDataConfig));
map.put("retriesOnError", retriesOnError);
map.put("metadataHost", metadataHost);
map.put("metadataSource", metadataSource);
return map;
}
public String getScriptOrDefault() {
if (scriptContent == null && scriptPath == null) {
//when user clear the text on UI, it will be set to empty string
//so use null check will not overwrite user settings
return getDefaultDslScript();
} else {
return scriptContent;
}
}
public String getSplunkAppUrl() {
if (isEmpty(splunkAppUrl) && isNotEmpty(host)) {
return "http://" + host + ":8000/en-US/app/splunk_app_jenkins/";
}
return splunkAppUrl;
}
public String getAppUrlOrHelp() {
String url = getSplunkAppUrl();
if (isEmpty(url)) {
return "/plugin/splunk-devops/help-splunkAppUrl.html?";
}
return url;
}
public void setSplunkAppUrl(String splunkAppUrl) {
if (!isEmpty(splunkAppUrl) && !splunkAppUrl.endsWith("/")) {
splunkAppUrl += "/";
}
this.splunkAppUrl = splunkAppUrl;
}
private String getLocalHostName() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
return "jenkins";
}
}
public Set<MetaDataConfigItem> getMetadataItemSet() {
return metadataItemSet;
}
public String getMetadataHost() {
if (metadataHost != null) {
return metadataHost;
} else {
//backwards compatible
if (metaDataProperties != null && metaDataProperties.containsKey("host")) {
return metaDataProperties.getProperty("host");
} else {
String url = null;
JenkinsLocationConfiguration jenkinsLocation = JenkinsLocationConfiguration.get();
if (jenkinsLocation != null) {
url = jenkinsLocation.getUrl();
}
if (url != null && !url.startsWith("http://localhost")) {
try {
return (new URL(url)).getHost();
} catch (MalformedURLException e) {
//do not care,just ignore
}
}
return getLocalHostName();
}
}
}
public void setMetadataHost(String metadataHost) {
this.metadataHost = metadataHost;
}
public void setMetadataItemSet(Set<MetaDataConfigItem> metadataItemSet) {
this.metadataItemSet = metadataItemSet;
this.metaDataConfig = MetaDataConfigItem.toString(metadataItemSet);
}
public String getMetadataSource() {
if (metadataSource != null) {
return metadataSource;
} else if (metaDataProperties != null && metaDataProperties.containsKey("source")) {
return metaDataProperties.getProperty("source");
} else {
return "";
}
}
public String getMetadataSource(String suffix) {
return getMetadataSource() + JENKINS_SOURCE_SEP + suffix;
}
public void setMetadataSource(String metadataSource) {
this.metadataSource = metadataSource;
}
private void migrate() {
if (this.scriptContent != null) {
String hash = DigestUtils.md5Hex(this.scriptContent);
if (SCRIPT_TEXT_MD5_HASH.contains(hash)) { //previous versions' script hash, update to use new version
this.scriptContent = getDefaultDslScript();
}
}
this.metadataItemSet = MetaDataConfigItem.loadProps(this.metaDataConfig);
//migrate settings prior to 1.9.0
if (this.globalPipelineFilter == null) {
this.globalPipelineFilter = true;
}
}
public String getIgnoredJobs() {
return ignoredJobs;
}
public void setIgnoredJobs(String ignoredJobs) {
this.ignoredJobs = ignoredJobs;
}
public Boolean getGlobalPipelineFilter() {
return globalPipelineFilter;
}
public void setGlobalPipelineFilter(Boolean globalPipelineFilter) {
this.globalPipelineFilter = globalPipelineFilter;
}
public boolean isPipelineFilterEnabled() {
return Boolean.TRUE.equals(globalPipelineFilter);
}
}