Skip to content

Commit 43af22c

Browse files
committed
Merge branch 'master' of https://github.com/jenkinsci/support-core-plugin into requesterAuthentication
2 parents 19a0964 + 6a6bf31 commit 43af22c

File tree

10 files changed

+379
-27
lines changed

10 files changed

+379
-27
lines changed

src/main/java/com/cloudbees/jenkins/support/SupportAction.java

+167-1
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,22 @@
2929
import com.cloudbees.jenkins.support.api.Component;
3030
import com.cloudbees.jenkins.support.api.SupportProvider;
3131
import com.cloudbees.jenkins.support.filter.ContentFilters;
32+
import edu.umd.cs.findbugs.annotations.NonNull;
3233
import hudson.Extension;
3334
import hudson.model.Api;
3435
import hudson.model.Failure;
3536
import hudson.model.RootAction;
3637
import hudson.security.Permission;
3738
import jakarta.servlet.ServletException;
3839
import jakarta.servlet.ServletOutputStream;
40+
import jakarta.servlet.http.HttpServletResponse;
3941
import java.io.File;
4042
import java.io.FileInputStream;
4143
import java.io.FileOutputStream;
4244
import java.io.IOException;
45+
import java.nio.file.Files;
46+
import java.nio.file.Path;
47+
import java.nio.file.Paths;
4348
import java.util.ArrayList;
4449
import java.util.Arrays;
4550
import java.util.Collections;
@@ -50,18 +55,25 @@
5055
import java.util.List;
5156
import java.util.Map;
5257
import java.util.Set;
58+
import java.util.UUID;
59+
import java.util.concurrent.ConcurrentHashMap;
60+
import java.util.concurrent.TimeUnit;
5361
import java.util.logging.Level;
5462
import java.util.logging.Logger;
5563
import java.util.stream.Collectors;
5664
import java.util.zip.ZipEntry;
5765
import java.util.zip.ZipOutputStream;
5866
import jenkins.model.Jenkins;
67+
import jenkins.util.ProgressiveRendering;
68+
import jenkins.util.Timer;
69+
import net.sf.json.JSON;
5970
import net.sf.json.JSONObject;
6071
import org.apache.commons.io.FileUtils;
6172
import org.jvnet.localizer.Localizable;
6273
import org.kohsuke.accmod.Restricted;
6374
import org.kohsuke.accmod.restrictions.NoExternalUse;
6475
import org.kohsuke.stapler.DataBoundConstructor;
76+
import org.kohsuke.stapler.HttpRedirect;
6577
import org.kohsuke.stapler.HttpResponse;
6678
import org.kohsuke.stapler.HttpResponses;
6779
import org.kohsuke.stapler.QueryParameter;
@@ -90,6 +102,12 @@ public class SupportAction implements RootAction, StaplerProxy {
90102
*/
91103
private final Logger logger = Logger.getLogger(SupportAction.class.getName());
92104

105+
private static final Path SUPPORT_BUNDLE_CREATION_FOLDER =
106+
Paths.get(System.getProperty("java.io.tmpdir")).resolve("support-bundle");
107+
public static final String SYNC_SUPPORT_BUNDLE = "support-bundle.zip";
108+
109+
private final Map<UUID, SupportBundleAsyncGenerator> generatorByTaskId = new ConcurrentHashMap<>();
110+
93111
@Override
94112
@Restricted(NoExternalUse.class)
95113
public Object getTarget() {
@@ -316,6 +334,58 @@ public void doGenerateAllBundles(StaplerRequest2 req, StaplerResponse2 rsp) thro
316334
rsp.sendError(SC_BAD_REQUEST);
317335
return;
318336
}
337+
final List<Component> components = getComponents(req, json);
338+
prepareBundle(rsp, components);
339+
}
340+
341+
/**
342+
* Generates a support bundle with selected components from the UI. in async
343+
* @param req The stapler request
344+
* @param rsp The stapler response
345+
* @throws ServletException If an error occurred during form submission
346+
* @throws IOException If an input or output exception occurs
347+
*/
348+
@RequirePOST
349+
public HttpRedirect doGenerateBundleAsync(StaplerRequest2 req, StaplerResponse2 rsp)
350+
throws ServletException, IOException {
351+
JSONObject json = req.getSubmittedForm();
352+
if (!json.has("components")) {
353+
rsp.sendError(SC_BAD_REQUEST);
354+
return new HttpRedirect("support");
355+
}
356+
final List<Component> components = getComponents(req, json);
357+
UUID taskId = UUID.randomUUID();
358+
359+
// There are some components that need the request components to be processed
360+
// these components cannot be processed async
361+
// so process them first and then process the other components async
362+
List<Component> syncComponent =
363+
components.stream().filter(c -> !c.canBeGeneratedAsync()).toList();
364+
if (!syncComponent.isEmpty()) {
365+
Path outputDir = SUPPORT_BUNDLE_CREATION_FOLDER.resolve(taskId.toString());
366+
if (!Files.exists(outputDir)) {
367+
try {
368+
Files.createDirectories(outputDir);
369+
} catch (IOException e) {
370+
throw new IOException("Failed to create directory: " + outputDir.toAbsolutePath(), e);
371+
}
372+
}
373+
try (FileOutputStream fileOutputStream =
374+
new FileOutputStream(new File(outputDir.toString(), SYNC_SUPPORT_BUNDLE))) {
375+
SupportPlugin.writeBundleForSyncComponents(fileOutputStream, syncComponent);
376+
} finally {
377+
logger.fine("Processing support bundle sunc completed");
378+
}
379+
}
380+
381+
// Process the remaining components that can be process async
382+
SupportBundleAsyncGenerator supportBundleAsyncGenerator = new SupportBundleAsyncGenerator();
383+
supportBundleAsyncGenerator.init(taskId, components);
384+
generatorByTaskId.put(taskId, supportBundleAsyncGenerator);
385+
return new HttpRedirect("progressPage?taskId=" + taskId);
386+
}
387+
388+
private List<Component> getComponents(StaplerRequest2 req, JSONObject json) throws IOException {
319389
logger.fine("Parsing request...");
320390
Set<String> remove = new HashSet<>();
321391
for (Selection s : req.bindJSONToList(Selection.class, json.get("components"))) {
@@ -341,7 +411,7 @@ public void doGenerateAllBundles(StaplerRequest2 req, StaplerResponse2 rsp) thro
341411
if (supportPlugin != null) {
342412
supportPlugin.setExcludedComponents(remove);
343413
}
344-
prepareBundle(rsp, components);
414+
return components;
345415
}
346416

347417
/**
@@ -429,4 +499,100 @@ public boolean isSelected() {
429499
return selected;
430500
}
431501
}
502+
503+
public ProgressiveRendering getGeneratorByTaskId(String taskId) throws Exception {
504+
return generatorByTaskId.get(UUID.fromString(taskId));
505+
}
506+
507+
public static class SupportBundleAsyncGenerator extends ProgressiveRendering {
508+
private final Logger logger = Logger.getLogger(SupportAction.class.getName());
509+
private UUID taskId;
510+
private boolean isCompleted;
511+
private List<Component> components;
512+
private boolean supportBundleGenerationInProgress = false;
513+
private String supportBundleName;
514+
515+
public SupportBundleAsyncGenerator init(UUID taskId, List<Component> components) {
516+
this.taskId = taskId;
517+
this.components = components;
518+
return this;
519+
}
520+
521+
@Override
522+
protected void compute() throws Exception {
523+
if (supportBundleGenerationInProgress) {
524+
logger.fine("Support bundle generation already in progress, for task id " + taskId);
525+
return;
526+
}
527+
528+
this.supportBundleName = BundleFileName.generate();
529+
this.supportBundleGenerationInProgress = true;
530+
logger.fine("Generating support bundle... task id " + taskId);
531+
Path outputDir = SUPPORT_BUNDLE_CREATION_FOLDER.resolve(taskId.toString());
532+
if (!Files.exists(outputDir)) {
533+
try {
534+
Files.createDirectories(outputDir);
535+
} catch (IOException e) {
536+
throw new IOException("Failed to create directory: " + outputDir.toAbsolutePath(), e);
537+
}
538+
}
539+
540+
try (FileOutputStream fileOutputStream =
541+
new FileOutputStream(new File(outputDir.toString(), supportBundleName))) {
542+
SupportPlugin.writeBundle(fileOutputStream, components, this::progress, outputDir);
543+
} finally {
544+
logger.fine("Processing support bundle async completed");
545+
}
546+
547+
isCompleted = true;
548+
}
549+
550+
@NonNull
551+
@Override
552+
protected JSON data() {
553+
JSONObject json = new JSONObject();
554+
json.put("isCompleted", isCompleted);
555+
json.put("taskId", String.valueOf(taskId));
556+
return json;
557+
}
558+
559+
public String getSupportBundleName() {
560+
return supportBundleName;
561+
}
562+
}
563+
564+
public void doDownloadBundle(@QueryParameter("taskId") String taskId, StaplerResponse2 rsp) throws IOException {
565+
String supportBundleName =
566+
generatorByTaskId.get(UUID.fromString(taskId)).getSupportBundleName();
567+
568+
File bundleFile = new File(SUPPORT_BUNDLE_CREATION_FOLDER + "/" + taskId + "/" + supportBundleName);
569+
if (!bundleFile.exists()) {
570+
rsp.sendError(HttpServletResponse.SC_NOT_FOUND, "Support bundle file not found");
571+
return;
572+
}
573+
574+
rsp.setContentType("application/zip");
575+
rsp.addHeader("Content-Disposition", "attachment; filename=" + supportBundleName);
576+
try (ServletOutputStream outputStream = rsp.getOutputStream()) {
577+
Files.copy(bundleFile.toPath(), outputStream);
578+
}
579+
580+
// Clean up temporary files after assembling the full bundle
581+
Timer.get()
582+
.schedule(
583+
() -> {
584+
File outputDir = new File(SUPPORT_BUNDLE_CREATION_FOLDER + "/" + taskId);
585+
586+
try {
587+
FileUtils.deleteDirectory(outputDir);
588+
generatorByTaskId.remove(taskId);
589+
logger.fine(() -> "Cleaned up temporary directory " + outputDir);
590+
591+
} catch (IOException e) {
592+
logger.log(Level.WARNING, () -> "Unable to delete " + outputDir);
593+
}
594+
},
595+
15,
596+
TimeUnit.MINUTES);
597+
}
432598
}

src/main/java/com/cloudbees/jenkins/support/SupportPlugin.java

+100-8
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
package com.cloudbees.jenkins.support;
2626

27+
import static com.cloudbees.jenkins.support.SupportAction.SYNC_SUPPORT_BUNDLE;
28+
2729
import com.cloudbees.jenkins.support.api.Component;
2830
import com.cloudbees.jenkins.support.api.ComponentVisitor;
2931
import com.cloudbees.jenkins.support.api.Container;
@@ -69,6 +71,7 @@
6971
import hudson.triggers.SafeTimerTask;
7072
import java.io.BufferedOutputStream;
7173
import java.io.File;
74+
import java.io.FileInputStream;
7275
import java.io.FileNotFoundException;
7376
import java.io.FileOutputStream;
7477
import java.io.IOException;
@@ -97,12 +100,14 @@
97100
import java.util.concurrent.TimeUnit;
98101
import java.util.concurrent.TimeoutException;
99102
import java.util.concurrent.atomic.AtomicLong;
103+
import java.util.function.DoubleConsumer;
100104
import java.util.logging.Handler;
101105
import java.util.logging.Level;
102106
import java.util.logging.LogRecord;
103107
import java.util.logging.Logger;
104108
import java.util.stream.Collectors;
105109
import java.util.zip.ZipEntry;
110+
import java.util.zip.ZipInputStream;
106111
import java.util.zip.ZipOutputStream;
107112
import jenkins.metrics.impl.JenkinsMetricProviderImpl;
108113
import jenkins.model.GlobalConfiguration;
@@ -347,12 +352,74 @@ public static void writeBundle(OutputStream outputStream) throws IOException {
347352
*/
348353
public static void writeBundle(OutputStream outputStream, final List<? extends Component> components)
349354
throws IOException {
350-
writeBundle(outputStream, components, new ComponentVisitor() {
351-
@Override
352-
public <T extends Component> void visit(Container container, T component) {
353-
component.addContents(container);
354-
}
355-
});
355+
writeBundle(
356+
outputStream,
357+
components,
358+
new ComponentVisitor() {
359+
@Override
360+
public <T extends Component> void visit(Container container, T component) {
361+
component.addContents(container);
362+
}
363+
},
364+
null,
365+
true);
366+
}
367+
368+
/**
369+
* Generate a bundle for the specified synchronous components.
370+
* This is useful when generating a support bundle asynchronously. There are some components that can't be generated
371+
* asynchronously as they need some context from the request. So these components must be first processed synchronously
372+
* and then the remaining components can be processed asynchronously.
373+
*
374+
* @param components a list of synchronous {@link Component} to include in the bundle
375+
* @throws IOException if an error occurs while generating the bundle.
376+
*/
377+
static void writeBundleForSyncComponents(OutputStream outputStream, final List<? extends Component> components)
378+
throws IOException {
379+
writeBundle(
380+
outputStream,
381+
components,
382+
new ComponentVisitor() {
383+
@Override
384+
public <T extends Component> void visit(Container container, T component) {
385+
component.addContents(container);
386+
}
387+
},
388+
null,
389+
false);
390+
}
391+
392+
/**
393+
* Generate a bundle for the specified components.
394+
*
395+
* @param components a list of {@link Component} to include in the bundle
396+
* @param progressCallback a callback to report progress back to the UI see ProgressiveRendering.progress
397+
* @param outputPath the path with the support bundle will be created in the cases of async generations
398+
* @throws IOException if an error occurs while generating the bundle.
399+
*/
400+
static void writeBundle(
401+
OutputStream outputStream,
402+
final List<? extends Component> components,
403+
DoubleConsumer progressCallback,
404+
Path outputPath)
405+
throws IOException {
406+
writeBundle(
407+
outputStream,
408+
components,
409+
new ComponentVisitor() {
410+
private final int totalComponents = components.size();
411+
private int currentIteration = 0;
412+
413+
@Override
414+
public <T extends Component> void visit(Container container, T component) {
415+
if (component.canBeGeneratedAsync()) {
416+
component.addContents(container);
417+
}
418+
progressCallback.accept((currentIteration++) / (double) totalComponents);
419+
}
420+
},
421+
outputPath,
422+
true);
356423
}
357424

358425
/**
@@ -361,10 +428,16 @@ public <T extends Component> void visit(Container container, T component) {
361428
* @param outputStream an {@link OutputStream}
362429
* @param components a list of {@link Component} to include in the bundle
363430
* @param componentConsumer a {@link ComponentVisitor}
431+
* @param outputPath the path with the support bundle will be created in the cases of async generations
432+
* set this to null, if generating support bundle synchronously
364433
* @throws IOException if an error occurs while generating the bundle.
365434
*/
366435
public static void writeBundle(
367-
OutputStream outputStream, final List<? extends Component> components, ComponentVisitor componentConsumer)
436+
OutputStream outputStream,
437+
final List<? extends Component> components,
438+
ComponentVisitor componentConsumer,
439+
Path outputPath,
440+
boolean addManifest)
368441
throws IOException {
369442
StringBuilder manifest = new StringBuilder();
370443
StringWriter errors = new StringWriter();
@@ -385,7 +458,9 @@ public static void writeBundle(
385458
LOGGER.log(
386459
Level.FINE,
387460
"Took " + (System.currentTimeMillis() - startTime) + "ms to process all components");
388-
contents.add(new UnfilteredStringContent("manifest.md", manifest.toString()));
461+
if (addManifest) {
462+
contents.add(new UnfilteredStringContent("manifest.md", manifest.toString()));
463+
}
389464

390465
FilteredOutputStream textOut = new FilteredOutputStream(binaryOut, filter);
391466
OutputStreamSelector selector = new OutputStreamSelector(() -> binaryOut, () -> textOut);
@@ -439,6 +514,23 @@ public static void writeBundle(
439514
+ name);
440515
}
441516
}
517+
518+
// process for async components
519+
if (outputPath != null) {
520+
try {
521+
File zipFile = outputPath.resolve(SYNC_SUPPORT_BUNDLE).toFile();
522+
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
523+
ZipEntry entry;
524+
while ((entry = zis.getNextEntry()) != null) {
525+
binaryOut.putNextEntry(entry);
526+
zis.transferTo(binaryOut);
527+
}
528+
}
529+
} catch (Exception e) {
530+
LOGGER.log(Level.WARNING, "Error while processing sync components in async mode", e);
531+
}
532+
}
533+
442534
LOGGER.log(
443535
Level.FINE,
444536
"Took " + (System.currentTimeMillis() - startTime) + "ms" + " and generated "

0 commit comments

Comments
 (0)