Skip to content

Commit 6a6bf31

Browse files
authored
Merge pull request #617 from nevingeorgesunny/generate-support-bundle-async
Generate support bundle Async
2 parents 18abe6f + effed5e commit 6a6bf31

File tree

10 files changed

+389
-27
lines changed

10 files changed

+389
-27
lines changed

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

+177-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
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;
@@ -38,10 +39,14 @@
3839
import hudson.security.Permission;
3940
import jakarta.servlet.ServletException;
4041
import jakarta.servlet.ServletOutputStream;
42+
import jakarta.servlet.http.HttpServletResponse;
4143
import java.io.File;
4244
import java.io.FileInputStream;
4345
import java.io.FileOutputStream;
4446
import java.io.IOException;
47+
import java.nio.file.Files;
48+
import java.nio.file.Path;
49+
import java.nio.file.Paths;
4550
import java.util.ArrayList;
4651
import java.util.Arrays;
4752
import java.util.Collections;
@@ -52,18 +57,25 @@
5257
import java.util.List;
5358
import java.util.Map;
5459
import java.util.Set;
60+
import java.util.UUID;
61+
import java.util.concurrent.ConcurrentHashMap;
62+
import java.util.concurrent.TimeUnit;
5563
import java.util.logging.Level;
5664
import java.util.logging.Logger;
5765
import java.util.stream.Collectors;
5866
import java.util.zip.ZipEntry;
5967
import java.util.zip.ZipOutputStream;
6068
import jenkins.model.Jenkins;
69+
import jenkins.util.ProgressiveRendering;
70+
import jenkins.util.Timer;
71+
import net.sf.json.JSON;
6172
import net.sf.json.JSONObject;
6273
import org.apache.commons.io.FileUtils;
6374
import org.jvnet.localizer.Localizable;
6475
import org.kohsuke.accmod.Restricted;
6576
import org.kohsuke.accmod.restrictions.NoExternalUse;
6677
import org.kohsuke.stapler.DataBoundConstructor;
78+
import org.kohsuke.stapler.HttpRedirect;
6779
import org.kohsuke.stapler.HttpResponse;
6880
import org.kohsuke.stapler.HttpResponses;
6981
import org.kohsuke.stapler.QueryParameter;
@@ -92,6 +104,12 @@ public class SupportAction implements RootAction, StaplerProxy {
92104
*/
93105
private final Logger logger = Logger.getLogger(SupportAction.class.getName());
94106

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

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

0 commit comments

Comments
 (0)